mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-19 02:43:50 +02:00
Merge pull request #39 from Iconica-Development/bugfix/poi
fix: service naming, image loading indicator
This commit is contained in:
commit
28e307cf90
81 changed files with 1400 additions and 1152 deletions
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
|
@ -2,22 +2,21 @@ version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "pub"
|
- package-ecosystem: "pub"
|
||||||
directory: "/packages/flutter_community_chat"
|
directory: "/packages/flutter_chat"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
|
||||||
- package-ecosystem: "pub"
|
- package-ecosystem: "pub"
|
||||||
directory: "/packages/flutter_community_chat_firebase"
|
directory: "/packages/flutter_chat_firebase"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
|
||||||
- package-ecosystem: "pub"
|
- package-ecosystem: "pub"
|
||||||
directory: "/packages/flutter_community_chat_interface"
|
directory: "/packages/flutter_chat_interface"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
|
||||||
- package-ecosystem: "pub"
|
- package-ecosystem: "pub"
|
||||||
directory: "/packages/flutter_community_chat_view"
|
directory: "/packages/flutter_chat_view"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
|
||||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -37,9 +37,9 @@ build/
|
||||||
.metadata
|
.metadata
|
||||||
|
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
packages/flutter_community_chat/pubspec.lock
|
packages/flutter_chat/pubspec.lock
|
||||||
packages/flutter_community_chat_firebase/pubspec.lock
|
packages/flutter_chat_firebase/pubspec.lock
|
||||||
packages/flutter_community_chat_interface/pubspec.lock
|
packages/flutter_chat_interface/pubspec.lock
|
||||||
packages/flutter_community_chat_view/pubspec.lock
|
packages/flutter_chat_view/pubspec.lock
|
||||||
|
|
||||||
pubspec_overrides.yaml
|
pubspec_overrides.yaml
|
||||||
|
|
78
README.md
78
README.md
|
@ -1,34 +1,52 @@
|
||||||
# Flutter Community Chat
|
# Flutter Chat
|
||||||
|
|
||||||
Flutter Community Chat is a package which gives the possibility to add a (personal or group) chat to your Flutter-application. Default this package adds support for a Firebase back-end. You can add your custom back-end (like a Websocket-API) by extending the `CommunityChatInterface` interface from the `flutter_community_chat_interface` package.
|
Flutter Chat is a package which gives the possibility to add a (personal or group) chat to your Flutter-application. Default this package adds support for a Firebase back-end. You can add your custom back-end (like a Websocket-API) by extending the `ChatInterface` interface from the `flutter_chat_interface` package.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Figma Design that defines this component (only accessible for Iconica developers): https://www.figma.com/file/4WkjwynOz5wFeFBRqTHPeP/Iconica-Design-System?type=design&node-id=357%3A3342&mode=design&t=XulkAJNPQ32ARxWh-1
|
Figma Design that defines this component (only accessible for Iconica developers): https://www.figma.com/file/4WkjwynOz5wFeFBRqTHPeP/Iconica-Design-System?type=design&node-id=357%3A3342&mode=design&t=XulkAJNPQ32ARxWh-1
|
||||||
Figma clickable prototype that demonstrates this component (only accessible for Iconica developers): https://www.figma.com/proto/PRJoVXQ5aOjAICfkQdAq2A/Iconica-User-Stories?page-id=1%3A2&type=design&node-id=56-6837&viewport=279%2C2452%2C0.2&t=E7Al3Xng2WXnbCEQ-1&scaling=scale-down&starting-point-node-id=56%3A6837&mode=design
|
Figma clickable prototype that demonstrates this component (only accessible for Iconica developers): https://www.figma.com/proto/PRJoVXQ5aOjAICfkQdAq2A/Iconica-User-Stories?page-id=1%3A2&type=design&node-id=56-6837&viewport=279%2C2452%2C0.2&t=E7Al3Xng2WXnbCEQ-1&scaling=scale-down&starting-point-node-id=56%3A6837&mode=design
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
To use this package, add flutter_community_chat as a dependency in your pubspec.yaml file:
|
To use this package, add flutter_chat as a dependency in your pubspec.yaml file:
|
||||||
|
|
||||||
```
|
```
|
||||||
flutter_community_chat:
|
flutter_chat:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat.git
|
url: https://github.com/Iconica-Development/flutter_chat
|
||||||
path: packages/flutter_community_chat
|
path: packages/flutter_chat
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are going to use Firebase as the back-end of the Community Chat, you should also add the following package as a dependency to your pubspec.yaml file:
|
If you are going to use Firebase as the back-end of the Community Chat, you should also add the following package as a dependency to your pubspec.yaml file:
|
||||||
|
|
||||||
```
|
```
|
||||||
flutter_community_chat_firebase:
|
flutter_chat_firebase:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat.git
|
url: https://github.com/Iconica-Development/flutter_chat
|
||||||
path: packages/flutter_community_chat_firebase
|
path: packages/flutter_chat_firebase
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a Firebase project for your application and add firebase firestore and storage.
|
Create a Firebase project for your application and add firebase firestore and storage.
|
||||||
|
|
||||||
|
To use the camera or photo library to send photos add the following to your project:
|
||||||
|
|
||||||
|
For ios add the following lines to your info.plist:
|
||||||
|
|
||||||
|
```
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Access camera</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Library</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
For android add the following lines to your AndroidManifest.xml:
|
||||||
|
|
||||||
|
```
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.GALLERY"/>
|
||||||
|
```
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
To use the module within your Flutter-application with predefined `Go_router` routes you should add the following:
|
To use the module within your Flutter-application with predefined `Go_router` routes you should add the following:
|
||||||
|
@ -37,18 +55,15 @@ Add go_router as dependency to your project.
|
||||||
Add the following configuration to your flutter_application:
|
Add the following configuration to your flutter_application:
|
||||||
|
|
||||||
```
|
```
|
||||||
List<GoRoute> getCommunityChatRoutes() => getCommunityChatStoryRoutes(
|
List<GoRoute> getChatRoutes() => getChatStoryRoutes(
|
||||||
CommunityChatUserStoryConfiguration(
|
ChatUserStoryConfiguration(
|
||||||
service: FirebaseChatService(userService: FirebaseUserService()),
|
chatService: chatService,
|
||||||
userService: FirebaseUserService(),
|
|
||||||
messageService:
|
|
||||||
FirebaseMessageService(userService: FirebaseUserService()),
|
|
||||||
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the `getCommunityChatRoutes()` to your go_router routes like so:
|
Add the `getChatRoutes()` to your go_router routes like so:
|
||||||
|
|
||||||
```
|
```
|
||||||
final GoRouter _router = GoRouter(
|
final GoRouter _router = GoRouter(
|
||||||
|
@ -61,13 +76,14 @@ final GoRouter _router = GoRouter(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
...getCommunityChatRoutes()
|
...getChatRoutes()
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
To use the module within your Flutter-application without predefined `Go_router` routes add the following code to the build-method of a chosen widget:
|
To use the module within your Flutter-application without predefined `Go_router` routes add the following code to the build-method of a chosen widget:
|
||||||
|
|
||||||
|
The `ChatScreen` shows all chats that you currently have with their latest messages.
|
||||||
To add the `ChatScreen` add the following code:
|
To add the `ChatScreen` add the following code:
|
||||||
|
|
||||||
````
|
````
|
||||||
|
@ -81,6 +97,7 @@ ChatScreen(
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `ChatDetailScreen` shows the messages that are in the current chat you selected.
|
||||||
To add the `ChatDetailScreen` add the following code:
|
To add the `ChatDetailScreen` add the following code:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -90,12 +107,10 @@ ChatDetailScreen(
|
||||||
onUploadImage: onUploadImage,
|
onUploadImage: onUploadImage,
|
||||||
onReadChat: onReadChat,
|
onReadChat: onReadChat,
|
||||||
service: service,
|
service: service,
|
||||||
chatUserService: chatUserService,
|
|
||||||
messageService: messageService,
|
|
||||||
pageSize: pageSize,
|
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On the `NewChatScreen` you can select a person to chat.
|
||||||
To add the `NewChatScreen` add the following code:
|
To add the `NewChatScreen` add the following code:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -103,7 +118,20 @@ NewChatScreen(
|
||||||
options: options,
|
options: options,
|
||||||
onPressCreateChat: onPressCreateChat,
|
onPressCreateChat: onPressCreateChat,
|
||||||
service: service,
|
service: service,
|
||||||
userService: userService,
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ChatEntryWidget` is a widget you can put anywhere in your app.
|
||||||
|
It displays the amount of unread messages you currently have.
|
||||||
|
You can choose to add a onTap to the `ChatEntryWidget` so it routes to the `ChatScreen`,
|
||||||
|
where all your chats are shown.
|
||||||
|
|
||||||
|
To add the `ChatEntryWidget` add the follwoing code:
|
||||||
|
|
||||||
|
```
|
||||||
|
ChatEntryWidget(
|
||||||
|
chatService: chatService,
|
||||||
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -121,13 +149,13 @@ The `ImagePickerTheme` also has its own parameters, how to use these parameters
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_community_chat/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
|
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_chat/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
|
||||||
|
|
||||||
## Want to contribute
|
## Want to contribute
|
||||||
|
|
||||||
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](../CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_community_chat/pulls).
|
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](../CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_chat/pulls).
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
This `flutter_community_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
|
This `flutter_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
|
||||||
````
|
````
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: flutter_community_chat
|
name: flutter_chat
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- packages/**
|
- packages/**
|
||||||
|
@ -21,7 +21,7 @@ scripts:
|
||||||
run: melos exec -c 1 -- "flutter pub upgrade"
|
run: melos exec -c 1 -- "flutter pub upgrade"
|
||||||
|
|
||||||
create:
|
create:
|
||||||
# run create in the example folder of flutter_community_chat_view
|
# run create in the example folder of flutter_chat_view
|
||||||
run: melos exec --scope="*example*" -c 1 -- "flutter create ."
|
run: melos exec --scope="*example*" -c 1 -- "flutter create ."
|
||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
|
|
11
packages/flutter_chat/lib/flutter_chat.dart
Normal file
11
packages/flutter_chat/lib/flutter_chat.dart
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
library flutter_chat;
|
||||||
|
|
||||||
|
export 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
export 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
export 'package:flutter_chat/src/routes.dart';
|
||||||
|
export 'package:flutter_chat/src/models/chat_configuration.dart';
|
||||||
|
export 'package:flutter_chat/src/flutter_chat_userstory.dart';
|
|
@ -3,40 +3,35 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat/src/models/community_chat_configuration.dart';
|
import 'package:flutter_chat/flutter_chat.dart';
|
||||||
import 'package:flutter_community_chat/src/go_router.dart';
|
import 'package:flutter_chat/src/go_router.dart';
|
||||||
import 'package:flutter_community_chat/src/routes.dart';
|
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
List<GoRoute> getCommunityChatStoryRoutes(
|
List<GoRoute> getChatStoryRoutes(
|
||||||
CommunityChatUserStoryConfiguration configuration,
|
ChatUserStoryConfiguration configuration,
|
||||||
) =>
|
) =>
|
||||||
<GoRoute>[
|
<GoRoute>[
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: CommunityChatUserStoryRoutes.chatScreen,
|
path: ChatUserStoryRoutes.chatScreen,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
var chatScreen = ChatScreen(
|
var chatScreen = ChatScreen(
|
||||||
pageSize: configuration.pageSize,
|
service: configuration.chatService,
|
||||||
service: configuration.service,
|
|
||||||
options: configuration.chatOptionsBuilder(context),
|
options: configuration.chatOptionsBuilder(context),
|
||||||
onNoChats: () async =>
|
onNoChats: () async =>
|
||||||
await context.push(CommunityChatUserStoryRoutes.newChatScreen),
|
await context.push(ChatUserStoryRoutes.newChatScreen),
|
||||||
onPressStartChat: () async {
|
onPressStartChat: () async {
|
||||||
if (configuration.onPressStartChat != null) {
|
if (configuration.onPressStartChat != null) {
|
||||||
return await configuration.onPressStartChat?.call();
|
return await configuration.onPressStartChat?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await context
|
return await context.push(ChatUserStoryRoutes.newChatScreen);
|
||||||
.push(CommunityChatUserStoryRoutes.newChatScreen);
|
|
||||||
},
|
},
|
||||||
onPressChat: (chat) =>
|
onPressChat: (chat) =>
|
||||||
configuration.onPressChat?.call(context, chat) ??
|
configuration.onPressChat?.call(context, chat) ??
|
||||||
context.push(
|
context.push(ChatUserStoryRoutes.chatDetailViewPath(chat.id!)),
|
||||||
CommunityChatUserStoryRoutes.chatDetailViewPath(chat.id!)),
|
|
||||||
onDeleteChat: (chat) =>
|
onDeleteChat: (chat) =>
|
||||||
configuration.onDeleteChat?.call(context, chat) ??
|
configuration.onDeleteChat?.call(context, chat) ??
|
||||||
configuration.service.deleteChat(chat),
|
configuration.chatService.chatOverviewService.deleteChat(chat),
|
||||||
deleteChatDialog: configuration.deleteChatDialog,
|
deleteChatDialog: configuration.deleteChatDialog,
|
||||||
translations: configuration.translations,
|
translations: configuration.translations,
|
||||||
);
|
);
|
||||||
|
@ -54,35 +49,38 @@ List<GoRoute> getCommunityChatStoryRoutes(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: CommunityChatUserStoryRoutes.chatDetailScreen,
|
path: ChatUserStoryRoutes.chatDetailScreen,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
var chatId = state.pathParameters['id'];
|
var chatId = state.pathParameters['id'];
|
||||||
var chat = PersonalChatModel(user: ChatUserModel(), id: chatId);
|
|
||||||
var chatDetailScreen = ChatDetailScreen(
|
var chatDetailScreen = ChatDetailScreen(
|
||||||
pageSize: configuration.messagePageSize,
|
pageSize: configuration.messagePageSize,
|
||||||
options: configuration.chatOptionsBuilder(context),
|
options: configuration.chatOptionsBuilder(context),
|
||||||
translations: configuration.translations,
|
translations: configuration.translations,
|
||||||
chatUserService: configuration.userService,
|
service: configuration.chatService,
|
||||||
service: configuration.service,
|
chatId: chatId!,
|
||||||
messageService: configuration.messageService,
|
|
||||||
chat: chat,
|
|
||||||
onMessageSubmit: (message) async {
|
onMessageSubmit: (message) async {
|
||||||
configuration.onMessageSubmit?.call(message) ??
|
configuration.onMessageSubmit?.call(message) ??
|
||||||
configuration.messageService
|
configuration.chatService.chatDetailService
|
||||||
.sendTextMessage(chat: chat, text: message);
|
.sendTextMessage(chatId: chatId, text: message);
|
||||||
configuration.afterMessageSent?.call(chat);
|
configuration.afterMessageSent?.call(chatId);
|
||||||
},
|
},
|
||||||
onUploadImage: (image) async {
|
onUploadImage: (image) async {
|
||||||
configuration.onUploadImage?.call(image) ??
|
configuration.onUploadImage?.call(image) ??
|
||||||
configuration.messageService
|
configuration.chatService.chatDetailService
|
||||||
.sendImageMessage(chat: chat, image: image);
|
.sendImageMessage(chatId: chatId, image: image);
|
||||||
configuration.afterMessageSent?.call(chat);
|
configuration.afterMessageSent?.call(chatId);
|
||||||
},
|
},
|
||||||
onReadChat: (chat) =>
|
onReadChat: (chat) =>
|
||||||
configuration.onReadChat?.call(chat) ??
|
configuration.onReadChat?.call(chat) ??
|
||||||
configuration.service.readChat(chat),
|
configuration.chatService.chatOverviewService.readChat(chat),
|
||||||
onPressChatTitle: (context, chat) =>
|
onPressChatTitle: (context, chat) {
|
||||||
configuration.onPressChatTitle?.call(context, chat),
|
if (configuration.onPressChatTitle?.call(context, chat) != null) {
|
||||||
|
return configuration.onPressChatTitle?.call(context, chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.push(
|
||||||
|
ChatUserStoryRoutes.chatProfileScreenPath(chat.id!, null));
|
||||||
|
},
|
||||||
iconColor: configuration.iconColor,
|
iconColor: configuration.iconColor,
|
||||||
);
|
);
|
||||||
return buildScreenWithoutTransition(
|
return buildScreenWithoutTransition(
|
||||||
|
@ -99,19 +97,20 @@ List<GoRoute> getCommunityChatStoryRoutes(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: CommunityChatUserStoryRoutes.newChatScreen,
|
path: ChatUserStoryRoutes.newChatScreen,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
var newChatScreen = NewChatScreen(
|
var newChatScreen = NewChatScreen(
|
||||||
options: configuration.chatOptionsBuilder(context),
|
options: configuration.chatOptionsBuilder(context),
|
||||||
translations: configuration.translations,
|
translations: configuration.translations,
|
||||||
service: configuration.service,
|
service: configuration.chatService,
|
||||||
userService: configuration.userService,
|
|
||||||
onPressCreateChat: (user) async {
|
onPressCreateChat: (user) async {
|
||||||
configuration.onPressCreateChat?.call(user);
|
configuration.onPressCreateChat?.call(user);
|
||||||
if (configuration.onPressChat != null) return;
|
if (configuration.onPressChat != null) return;
|
||||||
var chat = await configuration.service.getChatByUser(user);
|
var chat = await configuration.chatService.chatOverviewService
|
||||||
|
.getChatByUser(user);
|
||||||
if (chat.id == null) {
|
if (chat.id == null) {
|
||||||
chat = await configuration.service.storeChatIfNot(
|
chat = await configuration.chatService.chatOverviewService
|
||||||
|
.storeChatIfNot(
|
||||||
PersonalChatModel(
|
PersonalChatModel(
|
||||||
user: user,
|
user: user,
|
||||||
),
|
),
|
||||||
|
@ -119,8 +118,7 @@ List<GoRoute> getCommunityChatStoryRoutes(
|
||||||
}
|
}
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await context.push(
|
await context.push(
|
||||||
CommunityChatUserStoryRoutes.chatDetailViewPath(
|
ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ''));
|
||||||
chat.id ?? ''));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return buildScreenWithoutTransition(
|
return buildScreenWithoutTransition(
|
||||||
|
@ -136,4 +134,38 @@ List<GoRoute> getCommunityChatStoryRoutes(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: ChatUserStoryRoutes.chatProfileScreen,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
var chatId = state.pathParameters['id'];
|
||||||
|
var userId = state.pathParameters['userId'];
|
||||||
|
var id = userId == 'null' ? null : userId;
|
||||||
|
var profileScreen = ChatProfileScreen(
|
||||||
|
translations: configuration.translations,
|
||||||
|
chatService: configuration.chatService,
|
||||||
|
chatId: chatId!,
|
||||||
|
userId: id,
|
||||||
|
onTapUser: (user) async {
|
||||||
|
if (configuration.onPressUserProfile != null) {
|
||||||
|
return configuration.onPressUserProfile!.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await context.push(
|
||||||
|
ChatUserStoryRoutes.chatProfileScreenPath(chatId, user),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return buildScreenWithoutTransition(
|
||||||
|
context: context,
|
||||||
|
state: state,
|
||||||
|
child: configuration.chatPageBuilder?.call(
|
||||||
|
context,
|
||||||
|
profileScreen,
|
||||||
|
) ??
|
||||||
|
Scaffold(
|
||||||
|
body: profileScreen,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
|
@ -5,14 +5,12 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class CommunityChatUserStoryConfiguration {
|
class ChatUserStoryConfiguration {
|
||||||
const CommunityChatUserStoryConfiguration({
|
const ChatUserStoryConfiguration({
|
||||||
required this.userService,
|
required this.chatService,
|
||||||
required this.messageService,
|
|
||||||
required this.service,
|
|
||||||
required this.chatOptionsBuilder,
|
required this.chatOptionsBuilder,
|
||||||
this.pageSize = 10,
|
this.pageSize = 10,
|
||||||
this.onPressStartChat,
|
this.onPressStartChat,
|
||||||
|
@ -31,10 +29,9 @@ class CommunityChatUserStoryConfiguration {
|
||||||
this.onPressChatTitle,
|
this.onPressChatTitle,
|
||||||
this.afterMessageSent,
|
this.afterMessageSent,
|
||||||
this.messagePageSize = 20,
|
this.messagePageSize = 20,
|
||||||
|
this.onPressUserProfile,
|
||||||
});
|
});
|
||||||
final ChatService service;
|
final ChatService chatService;
|
||||||
final ChatUserService userService;
|
|
||||||
final MessageService messageService;
|
|
||||||
final Function(BuildContext, ChatModel)? onPressChat;
|
final Function(BuildContext, ChatModel)? onPressChat;
|
||||||
final Function(BuildContext, ChatModel)? onDeleteChat;
|
final Function(BuildContext, ChatModel)? onDeleteChat;
|
||||||
final ChatTranslations translations;
|
final ChatTranslations translations;
|
||||||
|
@ -43,7 +40,7 @@ class CommunityChatUserStoryConfiguration {
|
||||||
final Future<void> Function(String text)? onMessageSubmit;
|
final Future<void> Function(String text)? onMessageSubmit;
|
||||||
|
|
||||||
/// Called after a new message is sent. This can be used to do something extra like sending a push notification.
|
/// Called after a new message is sent. This can be used to do something extra like sending a push notification.
|
||||||
final Function(ChatModel chat)? afterMessageSent;
|
final Function(String chatId)? afterMessageSent;
|
||||||
final Future<void> Function(ChatModel chat)? onReadChat;
|
final Future<void> Function(ChatModel chat)? onReadChat;
|
||||||
final Function(ChatUserModel)? onPressCreateChat;
|
final Function(ChatUserModel)? onPressCreateChat;
|
||||||
final ChatOptions Function(BuildContext context) chatOptionsBuilder;
|
final ChatOptions Function(BuildContext context) chatOptionsBuilder;
|
||||||
|
@ -58,4 +55,5 @@ class CommunityChatUserStoryConfiguration {
|
||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
final Widget Function(BuildContext context, Widget child)? chatPageBuilder;
|
final Widget Function(BuildContext context, Widget child)? chatPageBuilder;
|
||||||
final Function()? onPressStartChat;
|
final Function()? onPressStartChat;
|
||||||
|
final Function()? onPressUserProfile;
|
||||||
}
|
}
|
|
@ -2,9 +2,12 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
mixin CommunityChatUserStoryRoutes {
|
mixin ChatUserStoryRoutes {
|
||||||
static const String chatScreen = '/chat';
|
static const String chatScreen = '/chat';
|
||||||
static String chatDetailViewPath(String chatId) => '/chat-detail/$chatId';
|
static String chatDetailViewPath(String chatId) => '/chat-detail/$chatId';
|
||||||
static const String chatDetailScreen = '/chat-detail/:id';
|
static const String chatDetailScreen = '/chat-detail/:id';
|
||||||
static const String newChatScreen = '/new-chat';
|
static const String newChatScreen = '/new-chat';
|
||||||
|
static String chatProfileScreenPath(String chatId, String? userId) =>
|
||||||
|
'/chat-profile/$chatId/$userId';
|
||||||
|
static const String chatProfileScreen = '/chat-profile/:id/:userId';
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
name: flutter_community_chat
|
name: flutter_chat
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
|
@ -16,15 +16,15 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
go_router: any
|
go_router: any
|
||||||
flutter_community_chat_view:
|
flutter_chat_view:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat
|
url: https://github.com/Iconica-Development/flutter_chat
|
||||||
path: packages/flutter_community_chat_view
|
path: packages/flutter_chat_view
|
||||||
ref: 1.0.0
|
ref: 1.0.0
|
||||||
flutter_community_chat_interface:
|
flutter_chat_interface:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat
|
url: https://github.com/Iconica-Development/flutter_chat
|
||||||
path: packages/flutter_community_chat_interface
|
path: packages/flutter_chat_interface
|
||||||
ref: 1.0.0
|
ref: 1.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_firebase/dto/firebase_message_document.dart';
|
import 'package:flutter_chat_firebase/dto/firebase_message_document.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class FirebaseChatDocument {
|
class FirebaseChatDocument {
|
|
@ -0,0 +1,7 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
library flutter_chat_firebase;
|
||||||
|
|
||||||
|
export 'package:flutter_chat_firebase/service/service.dart';
|
|
@ -8,12 +8,14 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_storage/firebase_storage.dart';
|
import 'package:firebase_storage/firebase_storage.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart';
|
import 'package:flutter_chat_firebase/config/firebase_chat_options.dart';
|
||||||
import 'package:flutter_community_chat_firebase/dto/firebase_message_document.dart';
|
import 'package:flutter_chat_firebase/dto/firebase_message_document.dart';
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class FirebaseMessageService with ChangeNotifier implements MessageService {
|
class FirebaseChatDetailService
|
||||||
|
with ChangeNotifier
|
||||||
|
implements ChatDetailService {
|
||||||
late final FirebaseFirestore _db;
|
late final FirebaseFirestore _db;
|
||||||
late final FirebaseStorage _storage;
|
late final FirebaseStorage _storage;
|
||||||
late final ChatUserService _userService;
|
late final ChatUserService _userService;
|
||||||
|
@ -23,11 +25,11 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
StreamSubscription<QuerySnapshot>? _subscription;
|
StreamSubscription<QuerySnapshot>? _subscription;
|
||||||
DocumentSnapshot<Object>? lastMessage;
|
DocumentSnapshot<Object>? lastMessage;
|
||||||
List<ChatMessageModel> _cumulativeMessages = [];
|
List<ChatMessageModel> _cumulativeMessages = [];
|
||||||
ChatModel? lastChat;
|
String? lastChat;
|
||||||
int? chatPageSize;
|
int? chatPageSize;
|
||||||
DateTime timestampToFilter = DateTime.now();
|
DateTime timestampToFilter = DateTime.now();
|
||||||
|
|
||||||
FirebaseMessageService({
|
FirebaseChatDetailService({
|
||||||
required ChatUserService userService,
|
required ChatUserService userService,
|
||||||
FirebaseApp? app,
|
FirebaseApp? app,
|
||||||
FirebaseChatOptions? options,
|
FirebaseChatOptions? options,
|
||||||
|
@ -40,10 +42,10 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
_options = options ?? const FirebaseChatOptions();
|
_options = options ?? const FirebaseChatOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sendMessage(ChatModel chat, Map<String, dynamic> data) async {
|
Future<void> _sendMessage(String chatId, Map<String, dynamic> data) async {
|
||||||
var currentUser = await _userService.getCurrentUser();
|
var currentUser = await _userService.getCurrentUser();
|
||||||
|
|
||||||
if (chat.id == null || currentUser == null) {
|
if (currentUser == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +59,7 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
.collection(
|
.collection(
|
||||||
_options.chatsCollectionName,
|
_options.chatsCollectionName,
|
||||||
)
|
)
|
||||||
.doc(chat.id);
|
.doc(chatId);
|
||||||
|
|
||||||
var newMessage = await chatReference
|
var newMessage = await chatReference
|
||||||
.collection(
|
.collection(
|
||||||
|
@ -78,21 +80,13 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
.collection(
|
.collection(
|
||||||
_options.chatsMetaDataCollectionName,
|
_options.chatsMetaDataCollectionName,
|
||||||
)
|
)
|
||||||
.doc(chat.id);
|
.doc(chatId);
|
||||||
|
|
||||||
await metadataReference.update({
|
await metadataReference.update({
|
||||||
'last_used': DateTime.now(),
|
'last_used': DateTime.now(),
|
||||||
'last_message': message,
|
'last_message': message,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_controller != null) {
|
|
||||||
if (chat.id != null &&
|
|
||||||
_controller!.hasListener &&
|
|
||||||
(_subscription == null)) {
|
|
||||||
_subscription = _startListeningForMessages(chat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the chat counter for the other users
|
// update the chat counter for the other users
|
||||||
// get all users from the chat
|
// get all users from the chat
|
||||||
// there is a field in the chat document called users that has a list of user ids
|
// there is a field in the chat document called users that has a list of user ids
|
||||||
|
@ -107,7 +101,7 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
)
|
)
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection(_options.userChatsCollectionName)
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(chat.id);
|
.doc(chatId);
|
||||||
// what if the amount_unread_messages field does not exist?
|
// what if the amount_unread_messages field does not exist?
|
||||||
// it should be created when the chat is create
|
// it should be created when the chat is create
|
||||||
if ((await userReference.get())
|
if ((await userReference.get())
|
||||||
|
@ -120,7 +114,7 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
} else {
|
} else {
|
||||||
await userReference.set({
|
await userReference.set({
|
||||||
'amount_unread_messages': 1,
|
'amount_unread_messages': 1,
|
||||||
});
|
}, SetOptions(merge: true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,10 +123,10 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
@override
|
@override
|
||||||
Future<void> sendTextMessage({
|
Future<void> sendTextMessage({
|
||||||
required String text,
|
required String text,
|
||||||
required ChatModel chat,
|
required String chatId,
|
||||||
}) {
|
}) {
|
||||||
return _sendMessage(
|
return _sendMessage(
|
||||||
chat,
|
chatId,
|
||||||
{
|
{
|
||||||
'text': text,
|
'text': text,
|
||||||
},
|
},
|
||||||
|
@ -141,21 +135,17 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> sendImageMessage({
|
Future<void> sendImageMessage({
|
||||||
required ChatModel chat,
|
required String chatId,
|
||||||
required Uint8List image,
|
required Uint8List image,
|
||||||
}) async {
|
}) async {
|
||||||
if (chat.id == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ref = _storage
|
var ref = _storage
|
||||||
.ref('${_options.chatsCollectionName}/${chat.id}/${const Uuid().v4()}');
|
.ref('${_options.chatsCollectionName}/$chatId/${const Uuid().v4()}');
|
||||||
|
|
||||||
return ref.putData(image).then(
|
return ref.putData(image).then(
|
||||||
(_) => ref.getDownloadURL().then(
|
(_) => ref.getDownloadURL().then(
|
||||||
(url) {
|
(url) {
|
||||||
_sendMessage(
|
_sendMessage(
|
||||||
chat,
|
chatId,
|
||||||
{
|
{
|
||||||
'image_url': url,
|
'image_url': url,
|
||||||
},
|
},
|
||||||
|
@ -165,52 +155,16 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Query<FirebaseMessageDocument> _getMessagesQuery(ChatModel chat) {
|
|
||||||
if (lastChat == null) {
|
|
||||||
lastChat = chat;
|
|
||||||
} else if (lastChat?.id != chat.id) {
|
|
||||||
_cumulativeMessages = [];
|
|
||||||
lastChat = chat;
|
|
||||||
lastMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var query = _db
|
|
||||||
.collection(_options.chatsCollectionName)
|
|
||||||
.doc(chat.id)
|
|
||||||
.collection(_options.messagesCollectionName)
|
|
||||||
.orderBy('timestamp', descending: true)
|
|
||||||
.limit(chatPageSize!);
|
|
||||||
|
|
||||||
if (lastMessage == null) {
|
|
||||||
return query.withConverter<FirebaseMessageDocument>(
|
|
||||||
fromFirestore: (snapshot, _) =>
|
|
||||||
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
|
|
||||||
toFirestore: (user, _) => user.toJson(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
.startAfterDocument(lastMessage!)
|
|
||||||
.withConverter<FirebaseMessageDocument>(
|
|
||||||
fromFirestore: (snapshot, _) =>
|
|
||||||
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
|
|
||||||
toFirestore: (user, _) => user.toJson(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) {
|
Stream<List<ChatMessageModel>> getMessagesStream(String chatId) {
|
||||||
|
timestampToFilter = DateTime.now();
|
||||||
|
var messages = <ChatMessageModel>[];
|
||||||
_controller = StreamController<List<ChatMessageModel>>(
|
_controller = StreamController<List<ChatMessageModel>>(
|
||||||
onListen: () {
|
onListen: () {
|
||||||
var messagesCollection = _db
|
var messagesCollection = _db
|
||||||
.collection(_options.chatsCollectionName)
|
.collection(_options.chatsCollectionName)
|
||||||
.doc(chat.id)
|
.doc(chatId)
|
||||||
.collection(_options.messagesCollectionName)
|
.collection(_options.messagesCollectionName)
|
||||||
.withConverter<FirebaseMessageDocument>(
|
|
||||||
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
|
|
||||||
snapshot.data()!, snapshot.id),
|
|
||||||
toFirestore: (user, _) => user.toJson(),
|
|
||||||
);
|
|
||||||
var query = messagesCollection
|
|
||||||
.where(
|
.where(
|
||||||
'timestamp',
|
'timestamp',
|
||||||
isGreaterThan: timestampToFilter,
|
isGreaterThan: timestampToFilter,
|
||||||
|
@ -219,67 +173,45 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
|
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
|
||||||
snapshot.data()!, snapshot.id),
|
snapshot.data()!, snapshot.id),
|
||||||
toFirestore: (user, _) => user.toJson(),
|
toFirestore: (user, _) => user.toJson(),
|
||||||
);
|
)
|
||||||
|
.snapshots();
|
||||||
|
|
||||||
var stream = query.snapshots();
|
_subscription = messagesCollection.listen((event) async {
|
||||||
// Subscribe to the stream and process the updates
|
for (var message in event.docChanges) {
|
||||||
_subscription = stream.listen((snapshot) async {
|
var data = message.doc.data();
|
||||||
var messages = <ChatMessageModel>[];
|
var sender = await _userService.getUser(data!.sender);
|
||||||
|
|
||||||
for (var messageDoc in snapshot.docs) {
|
|
||||||
var messageData = messageDoc.data();
|
|
||||||
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||||
(messageData.timestamp).millisecondsSinceEpoch,
|
(data.timestamp).millisecondsSinceEpoch,
|
||||||
);
|
|
||||||
|
|
||||||
// Check if the message is already in the list to avoid duplicates
|
|
||||||
if (timestampToFilter.isBefore(timestamp)) {
|
|
||||||
if (!messages.any((message) {
|
|
||||||
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
(messageData.timestamp).millisecondsSinceEpoch,
|
|
||||||
);
|
|
||||||
return timestamp == message.timestamp;
|
|
||||||
})) {
|
|
||||||
var sender = await _userService.getUser(messageData.sender);
|
|
||||||
|
|
||||||
if (sender != null) {
|
|
||||||
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
(messageData.timestamp).millisecondsSinceEpoch,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (timestamp.isBefore(timestampToFilter)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
messages.add(
|
messages.add(
|
||||||
messageData.imageUrl != null
|
data.imageUrl != null
|
||||||
? ChatImageMessageModel(
|
? ChatImageMessageModel(
|
||||||
sender: sender,
|
sender: sender!,
|
||||||
imageUrl: messageData.imageUrl!,
|
imageUrl: data.imageUrl!,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
)
|
)
|
||||||
: ChatTextMessageModel(
|
: ChatTextMessageModel(
|
||||||
sender: sender,
|
sender: sender!,
|
||||||
text: messageData.text!,
|
text: data.text!,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
timestampToFilter = DateTime.now();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the filtered messages to the controller
|
|
||||||
_controller?.add(messages);
|
|
||||||
_cumulativeMessages = [
|
_cumulativeMessages = [
|
||||||
..._cumulativeMessages,
|
..._cumulativeMessages,
|
||||||
...messages,
|
...messages,
|
||||||
];
|
];
|
||||||
|
|
||||||
// remove all double elements
|
|
||||||
List<ChatMessageModel> uniqueObjects =
|
List<ChatMessageModel> uniqueObjects =
|
||||||
_cumulativeMessages.toSet().toList();
|
_cumulativeMessages.toSet().toList();
|
||||||
_cumulativeMessages = uniqueObjects;
|
_cumulativeMessages = uniqueObjects;
|
||||||
_cumulativeMessages
|
_cumulativeMessages
|
||||||
.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
timestampToFilter = DateTime.now();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onCancel: () {
|
onCancel: () {
|
||||||
|
@ -288,72 +220,25 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
debugPrint('Canceling messages stream');
|
debugPrint('Canceling messages stream');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return _controller!.stream;
|
return _controller!.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) {
|
@override
|
||||||
debugPrint('Start listening for messages in chat ${chat.id}');
|
void stopListeningForMessages() {
|
||||||
var snapshots = _getMessagesQuery(chat).snapshots();
|
_subscription?.cancel();
|
||||||
return snapshots.listen(
|
_subscription = null;
|
||||||
(snapshot) async {
|
_controller?.close();
|
||||||
List<ChatMessageModel> messages =
|
_controller = null;
|
||||||
List<ChatMessageModel>.from(_cumulativeMessages);
|
|
||||||
|
|
||||||
if (snapshot.docs.isNotEmpty) {
|
|
||||||
lastMessage = snapshot.docs.last;
|
|
||||||
|
|
||||||
for (var messageDoc in snapshot.docs) {
|
|
||||||
var messageData = messageDoc.data();
|
|
||||||
|
|
||||||
// Check if the message is already in the list to avoid duplicates
|
|
||||||
if (!messages.any((message) {
|
|
||||||
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
(messageData.timestamp).millisecondsSinceEpoch,
|
|
||||||
);
|
|
||||||
return timestamp == message.timestamp;
|
|
||||||
})) {
|
|
||||||
var sender = await _userService.getUser(messageData.sender);
|
|
||||||
|
|
||||||
if (sender != null) {
|
|
||||||
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
(messageData.timestamp).millisecondsSinceEpoch,
|
|
||||||
);
|
|
||||||
|
|
||||||
messages.add(
|
|
||||||
messageData.imageUrl != null
|
|
||||||
? ChatImageMessageModel(
|
|
||||||
sender: sender,
|
|
||||||
imageUrl: messageData.imageUrl!,
|
|
||||||
timestamp: timestamp,
|
|
||||||
)
|
|
||||||
: ChatTextMessageModel(
|
|
||||||
sender: sender,
|
|
||||||
text: messageData.text!,
|
|
||||||
timestamp: timestamp,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_cumulativeMessages = messages;
|
|
||||||
|
|
||||||
messages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
||||||
|
|
||||||
_controller?.add(messages);
|
|
||||||
notifyListeners();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> fetchMoreMessage(int pageSize, ChatModel chat) async {
|
Future<void> fetchMoreMessage(int pageSize, String chatId) async {
|
||||||
if (lastChat == null) {
|
if (lastChat == null) {
|
||||||
lastChat = chat;
|
lastChat = chatId;
|
||||||
} else if (lastChat?.id != chat.id) {
|
} else if (lastChat != chatId) {
|
||||||
_cumulativeMessages = [];
|
_cumulativeMessages = [];
|
||||||
lastChat = chat;
|
lastChat = chatId;
|
||||||
lastMessage = null;
|
lastMessage = null;
|
||||||
}
|
}
|
||||||
// get the x amount of last messages from the oldest message that is in cumulative messages and add that to the list
|
// get the x amount of last messages from the oldest message that is in cumulative messages and add that to the list
|
||||||
|
@ -361,7 +246,7 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
QuerySnapshot<FirebaseMessageDocument>? messagesQuerySnapshot;
|
QuerySnapshot<FirebaseMessageDocument>? messagesQuerySnapshot;
|
||||||
var query = _db
|
var query = _db
|
||||||
.collection(_options.chatsCollectionName)
|
.collection(_options.chatsCollectionName)
|
||||||
.doc(chat.id)
|
.doc(chatId)
|
||||||
.collection(_options.messagesCollectionName)
|
.collection(_options.messagesCollectionName)
|
||||||
.orderBy('timestamp', descending: true)
|
.orderBy('timestamp', descending: true)
|
||||||
.limit(pageSize);
|
.limit(pageSize);
|
||||||
|
@ -393,7 +278,6 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
List<FirebaseMessageDocument> messageDocuments = messagesQuerySnapshot.docs
|
List<FirebaseMessageDocument> messageDocuments = messagesQuerySnapshot.docs
|
||||||
.map((QueryDocumentSnapshot<FirebaseMessageDocument> doc) => doc.data())
|
.map((QueryDocumentSnapshot<FirebaseMessageDocument> doc) => doc.data())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
for (var message in messageDocuments) {
|
for (var message in messageDocuments) {
|
||||||
var sender = await _userService.getUser(message.sender);
|
var sender = await _userService.getUser(message.sender);
|
||||||
if (sender != null) {
|
if (sender != null) {
|
|
@ -1,26 +1,24 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_storage/firebase_storage.dart';
|
import 'package:firebase_storage/firebase_storage.dart';
|
||||||
import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart';
|
import 'package:flutter_chat_firebase/config/firebase_chat_options.dart';
|
||||||
import 'package:flutter_community_chat_firebase/dto/firebase_chat_document.dart';
|
import 'package:flutter_chat_firebase/dto/firebase_chat_document.dart';
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
class FirebaseChatService implements ChatService {
|
class FirebaseChatOverviewService implements ChatOverviewService {
|
||||||
late FirebaseFirestore _db;
|
late FirebaseFirestore _db;
|
||||||
late FirebaseStorage _storage;
|
late FirebaseStorage _storage;
|
||||||
late ChatUserService _userService;
|
late ChatUserService _userService;
|
||||||
late FirebaseChatOptions _options;
|
late FirebaseChatOptions _options;
|
||||||
DocumentSnapshot<Object?>? lastUserDocument;
|
|
||||||
String? lastGroupId;
|
|
||||||
List<String> chatIds = [];
|
|
||||||
int pageNumber = 1;
|
|
||||||
|
|
||||||
FirebaseChatService({
|
FirebaseChatOverviewService({
|
||||||
required ChatUserService userService,
|
required ChatUserService userService,
|
||||||
FirebaseApp? app,
|
FirebaseApp? app,
|
||||||
FirebaseChatOptions? options,
|
FirebaseChatOptions? options,
|
||||||
|
@ -33,266 +31,152 @@ class FirebaseChatService implements ChatService {
|
||||||
_options = options ?? const FirebaseChatOptions();
|
_options = options ?? const FirebaseChatOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<DocumentSnapshot> _addUnreadChatSubscription(
|
Future<int?> _addUnreadChatSubscription(
|
||||||
String chatId,
|
String chatId,
|
||||||
String userId,
|
String userId,
|
||||||
Function(int) onUnreadChatsUpdated,
|
) async {
|
||||||
) {
|
var snapshots = await _db
|
||||||
var snapshots = _db
|
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection(_options.userChatsCollectionName)
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(chatId)
|
.doc(chatId)
|
||||||
.snapshots();
|
.get();
|
||||||
|
|
||||||
return snapshots.listen((snapshot) {
|
return snapshots.data()?['amount_unread_messages'];
|
||||||
var data = snapshot.data();
|
|
||||||
onUnreadChatsUpdated(data?['amount_unread_messages'] ?? 0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<QuerySnapshot> _addChatSubscription(
|
@override
|
||||||
List<String> chatIds,
|
Stream<List<ChatModel>> getChatsStream() {
|
||||||
Function(List<ChatModel>) onReceivedChats,
|
StreamSubscription? chatSubscription;
|
||||||
) {
|
late StreamController<List<ChatModel>> controller;
|
||||||
var snapshots = _db
|
controller = StreamController(
|
||||||
|
onListen: () async {
|
||||||
|
var currentUser = await _userService.getCurrentUser();
|
||||||
|
var userSnapshot = _db
|
||||||
|
.collection(_options.usersCollectionName)
|
||||||
|
.doc(currentUser?.id)
|
||||||
|
.collection(_options.userChatsCollectionName)
|
||||||
|
.snapshots();
|
||||||
|
|
||||||
|
userSnapshot.listen((event) {
|
||||||
|
var chatIds = event.docs.map((e) => e.id).toList();
|
||||||
|
var chatSnapshot = _db
|
||||||
.collection(_options.chatsMetaDataCollectionName)
|
.collection(_options.chatsMetaDataCollectionName)
|
||||||
.where(
|
.where(
|
||||||
FieldPath.documentId,
|
FieldPath.documentId,
|
||||||
whereIn: chatIds,
|
whereIn: chatIds,
|
||||||
)
|
)
|
||||||
.withConverter(
|
.withConverter(
|
||||||
fromFirestore: (snapshot, _) =>
|
fromFirestore: (snapshot, _) => FirebaseChatDocument.fromJson(
|
||||||
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
|
snapshot.data()!, snapshot.id),
|
||||||
toFirestore: (chat, _) => chat.toJson(),
|
toFirestore: (chat, _) => chat.toJson(),
|
||||||
)
|
)
|
||||||
.snapshots();
|
.snapshots();
|
||||||
|
List<ChatModel> chats = [];
|
||||||
return snapshots.listen((snapshot) async {
|
|
||||||
var currentUser = await _userService.getCurrentUser();
|
|
||||||
var chats = <ChatModel>[];
|
|
||||||
|
|
||||||
for (var chatDoc in snapshot.docs) {
|
|
||||||
var chatData = chatDoc.data();
|
|
||||||
|
|
||||||
var messages = <ChatMessageModel>[];
|
|
||||||
|
|
||||||
if (chatData.lastMessage != null) {
|
|
||||||
var messageData = chatData.lastMessage!;
|
|
||||||
var sender = await _userService.getUser(messageData.sender);
|
|
||||||
|
|
||||||
if (sender != null) {
|
|
||||||
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
messageData.timestamp.millisecondsSinceEpoch,
|
|
||||||
);
|
|
||||||
|
|
||||||
messages.add(
|
|
||||||
messageData.imageUrl != null
|
|
||||||
? ChatImageMessageModel(
|
|
||||||
sender: sender,
|
|
||||||
imageUrl: messageData.imageUrl!,
|
|
||||||
timestamp: timestamp,
|
|
||||||
)
|
|
||||||
: ChatTextMessageModel(
|
|
||||||
sender: sender,
|
|
||||||
text: messageData.text!,
|
|
||||||
timestamp: timestamp,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ChatModel? chatModel;
|
ChatModel? chatModel;
|
||||||
|
|
||||||
if (chatData.personal) {
|
chatSubscription = chatSnapshot.listen((event) async {
|
||||||
var otherUserId = List<String>.from(chatData.users).firstWhere(
|
for (var element in event.docChanges) {
|
||||||
|
var chat = element.doc.data();
|
||||||
|
if (chat == null) return;
|
||||||
|
var otherUser = await _userService.getUser(
|
||||||
|
chat.users.firstWhere(
|
||||||
(element) => element != currentUser?.id,
|
(element) => element != currentUser?.id,
|
||||||
);
|
|
||||||
var otherUser = await _userService.getUser(otherUserId);
|
|
||||||
|
|
||||||
if (otherUser != null) {
|
|
||||||
chatModel = PersonalChatModel(
|
|
||||||
id: chatDoc.id,
|
|
||||||
user: otherUser,
|
|
||||||
lastMessage: messages.isNotEmpty ? messages.last : null,
|
|
||||||
messages: messages,
|
|
||||||
canBeDeleted: chatData.canBeDeleted,
|
|
||||||
lastUsed: chatData.lastUsed == null
|
|
||||||
? null
|
|
||||||
: DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
chatData.lastUsed!.millisecondsSinceEpoch,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
int? unread =
|
||||||
|
await _addUnreadChatSubscription(chat.id!, currentUser!.id!);
|
||||||
|
|
||||||
|
if (chat.personal) {
|
||||||
|
chatModel = PersonalChatModel(
|
||||||
|
id: chat.id,
|
||||||
|
user: otherUser!,
|
||||||
|
unreadMessages: unread,
|
||||||
|
lastUsed: chat.lastUsed == null
|
||||||
|
? null
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
chat.lastUsed!.millisecondsSinceEpoch,
|
||||||
|
),
|
||||||
|
lastMessage: chat.lastMessage != null &&
|
||||||
|
chat.lastMessage!.imageUrl != null
|
||||||
|
? ChatImageMessageModel(
|
||||||
|
sender: otherUser,
|
||||||
|
imageUrl: chat.lastMessage!.imageUrl!,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
chat.lastMessage!.timestamp.millisecondsSinceEpoch,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: chat.lastMessage != null
|
||||||
|
? ChatTextMessageModel(
|
||||||
|
sender: otherUser,
|
||||||
|
text: chat.lastMessage!.text!,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
chat.lastMessage!.timestamp
|
||||||
|
.millisecondsSinceEpoch,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// group chat
|
|
||||||
var users = <ChatUserModel>[];
|
var users = <ChatUserModel>[];
|
||||||
for (var userId in chatData.users) {
|
for (var userId in chat.users) {
|
||||||
var user = await _userService.getUser(userId);
|
var user = await _userService.getUser(userId);
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
users.add(user);
|
users.add(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chatModel = GroupChatModel(
|
chatModel = GroupChatModel(
|
||||||
id: chatDoc.id,
|
id: chat.id,
|
||||||
title: chatData.title ?? '',
|
title: chat.title ?? '',
|
||||||
imageUrl: chatData.imageUrl ?? '',
|
imageUrl: chat.imageUrl ?? '',
|
||||||
lastMessage: messages.isNotEmpty ? messages.last : null,
|
unreadMessages: unread,
|
||||||
messages: messages,
|
|
||||||
users: users,
|
users: users,
|
||||||
canBeDeleted: chatData.canBeDeleted,
|
lastMessage: chat.lastMessage != null &&
|
||||||
lastUsed: chatData.lastUsed == null
|
chat.lastMessage!.imageUrl == null
|
||||||
|
? ChatTextMessageModel(
|
||||||
|
sender: otherUser!,
|
||||||
|
text: chat.lastMessage!.text!,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
chat.lastMessage!.timestamp.millisecondsSinceEpoch,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ChatImageMessageModel(
|
||||||
|
sender: otherUser!,
|
||||||
|
imageUrl: chat.lastMessage!.imageUrl!,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
chat.lastMessage!.timestamp.millisecondsSinceEpoch,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
canBeDeleted: chat.canBeDeleted,
|
||||||
|
lastUsed: chat.lastUsed == null
|
||||||
? null
|
? null
|
||||||
: DateTime.fromMillisecondsSinceEpoch(
|
: DateTime.fromMillisecondsSinceEpoch(
|
||||||
chatData.lastUsed!.millisecondsSinceEpoch,
|
chat.lastUsed!.millisecondsSinceEpoch,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (chatModel != null) {
|
|
||||||
_addUnreadChatSubscription(chatModel.id ?? '', currentUser?.id ?? '',
|
|
||||||
(unreadMessages) {
|
|
||||||
// the chatmodel should be updated to reflect the amount of unread messages
|
|
||||||
if (chatModel is PersonalChatModel) {
|
|
||||||
chatModel = (chatModel as PersonalChatModel)
|
|
||||||
.copyWith(unreadMessages: unreadMessages);
|
|
||||||
} else if (chatModel is GroupChatModel) {
|
|
||||||
chatModel = (chatModel as GroupChatModel)
|
|
||||||
.copyWith(unreadMessages: unreadMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
chats = chats
|
|
||||||
.map((chat) => chat.id == chatModel?.id ? chatModel! : chat)
|
|
||||||
.toList();
|
|
||||||
onReceivedChats(chats);
|
|
||||||
});
|
|
||||||
chats.add(chatModel!);
|
chats.add(chatModel!);
|
||||||
}
|
}
|
||||||
|
Set<String> uniqueIds = <String>{};
|
||||||
|
List<ChatModel> uniqueChatModels = [];
|
||||||
|
|
||||||
|
for (ChatModel chatModel in chats) {
|
||||||
|
if (uniqueIds.add(chatModel.id!)) {
|
||||||
|
uniqueChatModels.add(chatModel);
|
||||||
}
|
}
|
||||||
onReceivedChats(chats);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
uniqueChatModels.sort(
|
||||||
List<List<String>> _splitChatIds({
|
|
||||||
required List<String> chatIds,
|
|
||||||
int chunkSize = 10,
|
|
||||||
}) {
|
|
||||||
var result = <List<String>>[];
|
|
||||||
var length = chatIds.length;
|
|
||||||
|
|
||||||
for (var i = 0; i < length; i += chunkSize) {
|
|
||||||
var lastIndex = i + chunkSize;
|
|
||||||
result.add(
|
|
||||||
chatIds.sublist(i, lastIndex > length ? length : lastIndex),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<List<ChatModel>> _getSpecificChatsStream(List<String> chatIds) {
|
|
||||||
late StreamController<List<ChatModel>> controller;
|
|
||||||
List<StreamSubscription<QuerySnapshot>> subscriptions = [];
|
|
||||||
var splittedChatIds = _splitChatIds(chatIds: chatIds);
|
|
||||||
|
|
||||||
controller = StreamController<List<ChatModel>>(
|
|
||||||
onListen: () {
|
|
||||||
var chats = <int, List<ChatModel>>{};
|
|
||||||
|
|
||||||
for (var chatIdPair in splittedChatIds.asMap().entries) {
|
|
||||||
subscriptions.add(
|
|
||||||
_addChatSubscription(
|
|
||||||
chatIdPair.value,
|
|
||||||
(data) {
|
|
||||||
chats[chatIdPair.key] = data;
|
|
||||||
|
|
||||||
var mergedChats = <ChatModel>[];
|
|
||||||
|
|
||||||
mergedChats.addAll(
|
|
||||||
chats.values.expand((element) => element),
|
|
||||||
);
|
|
||||||
|
|
||||||
mergedChats.sort(
|
|
||||||
(a, b) => (b.lastUsed ?? DateTime.now()).compareTo(
|
(a, b) => (b.lastUsed ?? DateTime.now()).compareTo(
|
||||||
a.lastUsed ?? DateTime.now(),
|
a.lastUsed ?? DateTime.now(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.add(mergedChats);
|
controller.add(uniqueChatModels);
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCancel: () {
|
|
||||||
for (var subscription in subscriptions) {
|
|
||||||
subscription.cancel();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return controller.stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<List<ChatModel>> getChatsStream(int pageSize) {
|
|
||||||
late StreamController<List<ChatModel>> controller;
|
|
||||||
StreamSubscription? chatsSubscription;
|
|
||||||
controller = StreamController(
|
|
||||||
onListen: () async {
|
|
||||||
QuerySnapshot<Map<String, dynamic>> userSnapshot;
|
|
||||||
List<String> userChatIds;
|
|
||||||
var currentUser = await _userService.getCurrentUser();
|
|
||||||
|
|
||||||
var userQuery = _db
|
|
||||||
.collection(_options.usersCollectionName)
|
|
||||||
.doc(currentUser?.id)
|
|
||||||
.collection(_options.userChatsCollectionName);
|
|
||||||
if (lastUserDocument == null) {
|
|
||||||
userSnapshot = await userQuery.limit(pageSize).get();
|
|
||||||
userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
|
|
||||||
} else {
|
|
||||||
userSnapshot = await userQuery
|
|
||||||
.limit(pageSize)
|
|
||||||
.startAfterDocument(lastUserDocument!)
|
|
||||||
.get();
|
|
||||||
userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
var userGroupChatIds = await _db
|
|
||||||
.collection(_options.usersCollectionName)
|
|
||||||
.doc(currentUser?.id)
|
|
||||||
.get()
|
|
||||||
.then((userCollection) =>
|
|
||||||
userCollection.data()?[_options.groupChatsCollectionName])
|
|
||||||
.then((groupChatLabels) => groupChatLabels?.cast<String>())
|
|
||||||
.then((groupChatIds) {
|
|
||||||
var startIndex = (pageNumber - 1) * pageSize;
|
|
||||||
var endIndex = startIndex + pageSize;
|
|
||||||
|
|
||||||
if (groupChatIds != null) {
|
|
||||||
if (startIndex >= groupChatIds.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
var groupIds = groupChatIds.sublist(
|
|
||||||
startIndex, endIndex.clamp(0, groupChatIds.length));
|
|
||||||
lastGroupId = groupIds.last;
|
|
||||||
return groupIds;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userSnapshot.docs.isNotEmpty) {
|
|
||||||
lastUserDocument = userSnapshot.docs.last;
|
|
||||||
}
|
|
||||||
|
|
||||||
pageNumber++;
|
|
||||||
chatIds.addAll([...userChatIds, ...userGroupChatIds]);
|
|
||||||
var chatsStream = _getSpecificChatsStream(chatIds);
|
|
||||||
|
|
||||||
chatsSubscription = chatsStream.listen((event) {
|
|
||||||
controller.add(event);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onCancel: () {
|
onCancel: () {
|
||||||
chatsSubscription?.cancel();
|
chatSubscription?.cancel();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return controller.stream;
|
return controller.stream;
|
||||||
|
@ -338,7 +222,7 @@ class FirebaseChatService implements ChatService {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var groupChatCollection = await _db
|
var groupChatCollection = await _db
|
||||||
.collection(_options.chatsCollectionName)
|
.collection(_options.chatsMetaDataCollectionName)
|
||||||
.doc(chatId)
|
.doc(chatId)
|
||||||
.withConverter(
|
.withConverter(
|
||||||
fromFirestore: (snapshot, _) =>
|
fromFirestore: (snapshot, _) =>
|
||||||
|
@ -442,11 +326,10 @@ class FirebaseChatService implements ChatService {
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection(_options.userChatsCollectionName)
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(reference.id)
|
.doc(reference.id)
|
||||||
.set({'users': userIds});
|
.set({'users': userIds}, SetOptions(merge: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
chat.id = reference.id;
|
chat.id = reference.id;
|
||||||
chatIds.add(chat.id!);
|
|
||||||
} else if (chat is GroupChatModel) {
|
} else if (chat is GroupChatModel) {
|
||||||
if (currentUser?.id == null) {
|
if (currentUser?.id == null) {
|
||||||
return chat;
|
return chat;
|
||||||
|
@ -479,11 +362,10 @@ class FirebaseChatService implements ChatService {
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection(_options.groupChatsCollectionName)
|
.collection(_options.groupChatsCollectionName)
|
||||||
.doc(reference.id)
|
.doc(reference.id)
|
||||||
.set({'users': userIds});
|
.set({'users': userIds}, SetOptions(merge: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
chat.id = reference.id;
|
chat.id = reference.id;
|
||||||
chatIds.add(chat.id!);
|
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Chat type not supported for firebase');
|
throw Exception('Chat type not supported for firebase');
|
||||||
}
|
}
|
|
@ -5,12 +5,12 @@
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart';
|
import 'package:flutter_chat_firebase/config/firebase_chat_options.dart';
|
||||||
import 'package:flutter_community_chat_firebase/dto/firebase_user_document.dart';
|
import 'package:flutter_chat_firebase/dto/firebase_user_document.dart';
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
class FirebaseUserService implements ChatUserService {
|
class FirebaseChatUserService implements ChatUserService {
|
||||||
FirebaseUserService({
|
FirebaseChatUserService({
|
||||||
FirebaseApp? app,
|
FirebaseApp? app,
|
||||||
FirebaseChatOptions? options,
|
FirebaseChatOptions? options,
|
||||||
}) {
|
}) {
|
3
packages/flutter_chat_firebase/lib/service/service.dart
Normal file
3
packages/flutter_chat_firebase/lib/service/service.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export 'package:flutter_chat_firebase/service/firebase_chat_user_service.dart';
|
||||||
|
export 'package:flutter_chat_firebase/service/firebase_chat_detail_service.dart';
|
||||||
|
export 'package:flutter_chat_firebase/service/firebase_chat_overview_service.dart';
|
|
@ -2,7 +2,7 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
name: flutter_community_chat_firebase
|
name: flutter_chat_firebase
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
@ -19,10 +19,10 @@ dependencies:
|
||||||
firebase_storage: ^11.0.5
|
firebase_storage: ^11.0.5
|
||||||
firebase_auth: ^4.1.2
|
firebase_auth: ^4.1.2
|
||||||
uuid: ^4.0.0
|
uuid: ^4.0.0
|
||||||
flutter_community_chat_interface:
|
flutter_chat_interface:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat
|
url: https://github.com/Iconica-Development/flutter_chat
|
||||||
path: packages/flutter_community_chat_interface
|
path: packages/flutter_chat_interface
|
||||||
ref: 1.0.0
|
ref: 1.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
library flutter_chat_interface;
|
||||||
|
|
||||||
|
export 'package:flutter_chat_interface/src/model/model.dart';
|
||||||
|
export 'package:flutter_chat_interface/src/service/service.dart';
|
||||||
|
export 'package:flutter_chat_interface/src/chat_data_provider.dart';
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
import 'package:flutter_data_interface/flutter_data_interface.dart';
|
import 'package:flutter_data_interface/flutter_data_interface.dart';
|
||||||
|
|
||||||
class ChatDataProvider extends DataInterface {
|
class ChatDataProvider extends DataInterface {
|
||||||
|
@ -14,6 +14,6 @@ class ChatDataProvider extends DataInterface {
|
||||||
|
|
||||||
static final Object _token = Object();
|
static final Object _token = Object();
|
||||||
final ChatUserService userService;
|
final ChatUserService userService;
|
||||||
final ChatService chatService;
|
final ChatOverviewService chatService;
|
||||||
final MessageService messageService;
|
final ChatDetailService messageService;
|
||||||
}
|
}
|
39
packages/flutter_chat_interface/lib/src/model/chat.dart
Normal file
39
packages/flutter_chat_interface/lib/src/model/chat.dart
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
|
abstract class ChatModelInterface {
|
||||||
|
String? get id;
|
||||||
|
List<ChatMessageModel>? get messages;
|
||||||
|
int? get unreadMessages;
|
||||||
|
DateTime? get lastUsed;
|
||||||
|
ChatMessageModel? get lastMessage;
|
||||||
|
bool get canBeDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatModel implements ChatModelInterface {
|
||||||
|
ChatModel({
|
||||||
|
this.id,
|
||||||
|
this.messages = const [],
|
||||||
|
this.unreadMessages,
|
||||||
|
this.lastUsed,
|
||||||
|
this.lastMessage,
|
||||||
|
this.canBeDeleted = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? id;
|
||||||
|
@override
|
||||||
|
final List<ChatMessageModel>? messages;
|
||||||
|
@override
|
||||||
|
final int? unreadMessages;
|
||||||
|
@override
|
||||||
|
final DateTime? lastUsed;
|
||||||
|
@override
|
||||||
|
final ChatMessageModel? lastMessage;
|
||||||
|
@override
|
||||||
|
final bool canBeDeleted;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
|
abstract class ChatImageMessageModelInterface extends ChatMessageModel {
|
||||||
|
ChatImageMessageModelInterface({
|
||||||
|
required super.sender,
|
||||||
|
required super.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatImageMessageModel implements ChatImageMessageModelInterface {
|
||||||
|
ChatImageMessageModel({
|
||||||
|
required this.sender,
|
||||||
|
required this.timestamp,
|
||||||
|
required this.imageUrl,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
final ChatUserModel sender;
|
||||||
|
@override
|
||||||
|
final DateTime timestamp;
|
||||||
|
@override
|
||||||
|
final String imageUrl;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter_chat_interface/src/model/chat_user.dart';
|
||||||
|
|
||||||
|
abstract class ChatMessageModelInterface {
|
||||||
|
ChatUserModel get sender;
|
||||||
|
DateTime get timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessageModel implements ChatMessageModelInterface {
|
||||||
|
ChatMessageModel({
|
||||||
|
required this.sender,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final ChatUserModel sender;
|
||||||
|
@override
|
||||||
|
final DateTime timestamp;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
|
abstract class ChatTextMessageModelInterface extends ChatMessageModel {
|
||||||
|
ChatTextMessageModelInterface({
|
||||||
|
required super.sender,
|
||||||
|
required super.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get text;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatTextMessageModel implements ChatTextMessageModelInterface {
|
||||||
|
ChatTextMessageModel({
|
||||||
|
required this.sender,
|
||||||
|
required this.timestamp,
|
||||||
|
required this.text,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
final ChatUserModel sender;
|
||||||
|
@override
|
||||||
|
final DateTime timestamp;
|
||||||
|
@override
|
||||||
|
final String text;
|
||||||
|
}
|
|
@ -1,8 +1,18 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
class ChatUserModel {
|
abstract class ChatUserModelInterface {
|
||||||
|
String? get id;
|
||||||
|
String? get firstName;
|
||||||
|
String? get lastName;
|
||||||
|
String? get imageUrl;
|
||||||
|
|
||||||
|
String? get fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatUserModel implements ChatUserModelInterface {
|
||||||
ChatUserModel({
|
ChatUserModel({
|
||||||
this.id,
|
this.id,
|
||||||
this.firstName,
|
this.firstName,
|
||||||
|
@ -10,11 +20,15 @@ class ChatUserModel {
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
final String? id;
|
final String? id;
|
||||||
|
@override
|
||||||
final String? firstName;
|
final String? firstName;
|
||||||
|
@override
|
||||||
final String? lastName;
|
final String? lastName;
|
||||||
|
@override
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
|
@override
|
||||||
String? get fullName {
|
String? get fullName {
|
||||||
var fullName = '';
|
var fullName = '';
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
|
abstract class GroupChatModelInterface extends ChatModel {
|
||||||
|
GroupChatModelInterface({
|
||||||
|
super.id,
|
||||||
|
super.messages,
|
||||||
|
super.lastUsed,
|
||||||
|
super.lastMessage,
|
||||||
|
super.unreadMessages,
|
||||||
|
super.canBeDeleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get title;
|
||||||
|
String get imageUrl;
|
||||||
|
List<ChatUserModel> get users;
|
||||||
|
|
||||||
|
GroupChatModelInterface copyWith({
|
||||||
|
String? id,
|
||||||
|
List<ChatMessageModel>? messages,
|
||||||
|
int? unreadMessages,
|
||||||
|
DateTime? lastUsed,
|
||||||
|
ChatMessageModel? lastMessage,
|
||||||
|
String? title,
|
||||||
|
String? imageUrl,
|
||||||
|
List<ChatUserModel>? users,
|
||||||
|
bool? canBeDeleted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupChatModel implements GroupChatModelInterface {
|
||||||
|
GroupChatModel({
|
||||||
|
this.id,
|
||||||
|
this.messages,
|
||||||
|
this.unreadMessages,
|
||||||
|
this.lastUsed,
|
||||||
|
this.lastMessage,
|
||||||
|
required this.canBeDeleted,
|
||||||
|
required this.title,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.users,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? id;
|
||||||
|
@override
|
||||||
|
final List<ChatMessageModel>? messages;
|
||||||
|
@override
|
||||||
|
final int? unreadMessages;
|
||||||
|
@override
|
||||||
|
final DateTime? lastUsed;
|
||||||
|
@override
|
||||||
|
final ChatMessageModel? lastMessage;
|
||||||
|
@override
|
||||||
|
final bool canBeDeleted;
|
||||||
|
@override
|
||||||
|
final String title;
|
||||||
|
@override
|
||||||
|
final String imageUrl;
|
||||||
|
@override
|
||||||
|
final List<ChatUserModel> users;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GroupChatModel copyWith({
|
||||||
|
String? id,
|
||||||
|
List<ChatMessageModel>? messages,
|
||||||
|
int? unreadMessages,
|
||||||
|
DateTime? lastUsed,
|
||||||
|
ChatMessageModel? lastMessage,
|
||||||
|
bool? canBeDeleted,
|
||||||
|
String? title,
|
||||||
|
String? imageUrl,
|
||||||
|
List<ChatUserModel>? users,
|
||||||
|
}) {
|
||||||
|
return GroupChatModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
messages: messages ?? this.messages,
|
||||||
|
unreadMessages: unreadMessages ?? this.unreadMessages,
|
||||||
|
lastUsed: lastUsed ?? this.lastUsed,
|
||||||
|
lastMessage: lastMessage ?? this.lastMessage,
|
||||||
|
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
|
||||||
|
title: title ?? this.title,
|
||||||
|
imageUrl: imageUrl ?? this.imageUrl,
|
||||||
|
users: users ?? this.users,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
|
abstract class PersonalChatModelInterface extends ChatModel {
|
||||||
|
PersonalChatModelInterface({
|
||||||
|
super.id,
|
||||||
|
super.messages,
|
||||||
|
super.unreadMessages,
|
||||||
|
super.lastUsed,
|
||||||
|
super.lastMessage,
|
||||||
|
super.canBeDeleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
ChatUserModel get user;
|
||||||
|
|
||||||
|
PersonalChatModel copyWith({
|
||||||
|
String? id,
|
||||||
|
List<ChatMessageModel>? messages,
|
||||||
|
int? unreadMessages,
|
||||||
|
DateTime? lastUsed,
|
||||||
|
ChatMessageModel? lastMessage,
|
||||||
|
ChatUserModel? user,
|
||||||
|
bool? canBeDeleted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PersonalChatModel implements PersonalChatModelInterface {
|
||||||
|
PersonalChatModel({
|
||||||
|
this.id,
|
||||||
|
this.messages = const [],
|
||||||
|
this.unreadMessages,
|
||||||
|
this.lastUsed,
|
||||||
|
this.lastMessage,
|
||||||
|
this.canBeDeleted = true,
|
||||||
|
required this.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? id;
|
||||||
|
@override
|
||||||
|
final List<ChatMessageModel>? messages;
|
||||||
|
@override
|
||||||
|
final int? unreadMessages;
|
||||||
|
@override
|
||||||
|
final DateTime? lastUsed;
|
||||||
|
@override
|
||||||
|
final ChatMessageModel? lastMessage;
|
||||||
|
@override
|
||||||
|
final bool canBeDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final ChatUserModel user;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PersonalChatModel copyWith({
|
||||||
|
String? id,
|
||||||
|
List<ChatMessageModel>? messages,
|
||||||
|
int? unreadMessages,
|
||||||
|
DateTime? lastUsed,
|
||||||
|
ChatMessageModel? lastMessage,
|
||||||
|
bool? canBeDeleted,
|
||||||
|
ChatUserModel? user,
|
||||||
|
}) {
|
||||||
|
return PersonalChatModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
messages: messages ?? this.messages,
|
||||||
|
unreadMessages: unreadMessages ?? this.unreadMessages,
|
||||||
|
lastUsed: lastUsed ?? this.lastUsed,
|
||||||
|
lastMessage: lastMessage ?? this.lastMessage,
|
||||||
|
user: user ?? this.user,
|
||||||
|
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,25 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
abstract class MessageService with ChangeNotifier {
|
abstract class ChatDetailService with ChangeNotifier {
|
||||||
Future<void> sendTextMessage({
|
Future<void> sendTextMessage({
|
||||||
required ChatModel chat,
|
required String chatId,
|
||||||
required String text,
|
required String text,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> sendImageMessage({
|
Future<void> sendImageMessage({
|
||||||
required ChatModel chat,
|
required String chatId,
|
||||||
required Uint8List image,
|
required Uint8List image,
|
||||||
});
|
});
|
||||||
|
|
||||||
Stream<List<ChatMessageModel>> getMessagesStream(
|
Stream<List<ChatMessageModel>> getMessagesStream(
|
||||||
ChatModel chat,
|
String chatId,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> fetchMoreMessage(int pageSize, ChatModel chat);
|
Future<void> fetchMoreMessage(int pageSize, String chatId);
|
||||||
|
|
||||||
List<ChatMessageModel> getMessages();
|
List<ChatMessageModel> getMessages();
|
||||||
|
|
||||||
|
void stopListeningForMessages();
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
abstract class ChatService {
|
abstract class ChatOverviewService {
|
||||||
Stream<List<ChatModel>> getChatsStream(int pageSize);
|
Stream<List<ChatModel>> getChatsStream();
|
||||||
Future<ChatModel> getChatByUser(ChatUserModel user);
|
Future<ChatModel> getChatByUser(ChatUserModel user);
|
||||||
Future<ChatModel> getChatById(String id);
|
Future<ChatModel> getChatById(String id);
|
||||||
Future<void> deleteChat(ChatModel chat);
|
Future<void> deleteChat(ChatModel chat);
|
|
@ -0,0 +1,14 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
|
class ChatService {
|
||||||
|
final ChatUserService chatUserService;
|
||||||
|
final ChatOverviewService chatOverviewService;
|
||||||
|
final ChatDetailService chatDetailService;
|
||||||
|
|
||||||
|
ChatService({
|
||||||
|
required this.chatUserService,
|
||||||
|
required this.chatOverviewService,
|
||||||
|
required this.chatDetailService,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export 'chat_overview_service.dart';
|
||||||
|
export 'user_service.dart';
|
||||||
|
export 'chat_detail_service.dart';
|
||||||
|
export 'chat_service.dart';
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
abstract class ChatUserService {
|
abstract class ChatUserService {
|
||||||
Future<ChatUserModel?> getUser(String id);
|
Future<ChatUserModel?> getUser(String id);
|
|
@ -2,7 +2,7 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
name: flutter_community_chat_interface
|
name: flutter_chat_interface
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
publish_to: none
|
publish_to: none
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MaterialApp(home: MyStatefulWidget()));
|
runApp(const MaterialApp(home: MyStatefulWidget()));
|
|
@ -2,7 +2,7 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
name: flutter_community_chat_view_example
|
name: flutter_chat_view_example
|
||||||
description: A standard flutter package.
|
description: A standard flutter package.
|
||||||
|
|
||||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
@ -14,7 +14,7 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_community_chat_view:
|
flutter_chat_view:
|
||||||
path: ..
|
path: ..
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
|
@ -1,6 +1,6 @@
|
||||||
// This is a generated file; do not edit or check into version control.
|
// This is a generated file; do not edit or check into version control.
|
||||||
FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.10.2/flutter
|
FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.10.2/flutter
|
||||||
FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_community_chat/packages/flutter_community_chat_view
|
FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_chat/packages/flutter_chat_view
|
||||||
COCOAPODS_PARALLEL_CODE_SIGN=true
|
COCOAPODS_PARALLEL_CODE_SIGN=true
|
||||||
FLUTTER_TARGET=lib/main.dart
|
FLUTTER_TARGET=lib/main.dart
|
||||||
FLUTTER_BUILD_DIR=build
|
FLUTTER_BUILD_DIR=build
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# This is a generated file; do not edit or check into version control.
|
# This is a generated file; do not edit or check into version control.
|
||||||
export "FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.10.2/flutter"
|
export "FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.10.2/flutter"
|
||||||
export "FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_community_chat/packages/flutter_community_chat_view"
|
export "FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_chat/packages/flutter_chat_view"
|
||||||
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||||
export "FLUTTER_TARGET=lib/main.dart"
|
export "FLUTTER_TARGET=lib/main.dart"
|
||||||
export "FLUTTER_BUILD_DIR=build"
|
export "FLUTTER_BUILD_DIR=build"
|
|
@ -2,13 +2,15 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
library flutter_community_chat_view;
|
library flutter_chat_view;
|
||||||
|
|
||||||
export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
export 'package:flutter_chat_interface/flutter_chat_interface.dart';
|
||||||
|
|
||||||
export 'src/components/chat_row.dart';
|
export 'src/components/chat_row.dart';
|
||||||
export 'src/config/chat_options.dart';
|
export 'src/config/chat_options.dart';
|
||||||
export 'src/config/chat_translations.dart';
|
export 'src/config/chat_translations.dart';
|
||||||
export 'src/screens/chat_detail_screen.dart';
|
export 'src/screens/chat_detail_screen.dart';
|
||||||
|
export 'src/screens/chat_entry_widget.dart';
|
||||||
|
export 'src/screens/chat_profile_screen.dart';
|
||||||
export 'src/screens/chat_screen.dart';
|
export 'src/screens/chat_screen.dart';
|
||||||
export 'src/screens/new_chat_screen.dart';
|
export 'src/screens/new_chat_screen.dart';
|
|
@ -3,7 +3,7 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
|
||||||
class ChatBottom extends StatefulWidget {
|
class ChatBottom extends StatefulWidget {
|
||||||
const ChatBottom({
|
const ChatBottom({
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
import 'package:flutter_community_chat_view/src/components/chat_image.dart';
|
import 'package:flutter_chat_view/src/components/chat_image.dart';
|
||||||
import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
|
import 'package:flutter_chat_view/src/services/date_formatter.dart';
|
||||||
|
|
||||||
class ChatDetailRow extends StatefulWidget {
|
class ChatDetailRow extends StatefulWidget {
|
||||||
const ChatDetailRow({
|
const ChatDetailRow({
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
|
||||||
SnackBar getImageLoadingSnackbar(ChatTranslations translations) => SnackBar(
|
SnackBar getImageLoadingSnackbar(ChatTranslations translations) => SnackBar(
|
||||||
duration: const Duration(minutes: 1),
|
duration: const Duration(minutes: 1),
|
|
@ -3,8 +3,8 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
import 'package:flutter_community_chat_view/src/components/chat_image.dart';
|
import 'package:flutter_chat_view/src/components/chat_image.dart';
|
||||||
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||||
|
|
||||||
class ChatOptions {
|
class ChatOptions {
|
|
@ -22,6 +22,7 @@ class ChatTranslations {
|
||||||
this.noUsersFound = 'No users found',
|
this.noUsersFound = 'No users found',
|
||||||
this.anonymousUser = 'Anonymous user',
|
this.anonymousUser = 'Anonymous user',
|
||||||
this.chatCantBeDeleted = 'This chat can\'t be deleted',
|
this.chatCantBeDeleted = 'This chat can\'t be deleted',
|
||||||
|
this.chatProfileUsers = 'Users:',
|
||||||
});
|
});
|
||||||
|
|
||||||
final String chatsTitle;
|
final String chatsTitle;
|
||||||
|
@ -40,6 +41,7 @@ class ChatTranslations {
|
||||||
final String deleteChatModalConfirm;
|
final String deleteChatModalConfirm;
|
||||||
final String noUsersFound;
|
final String noUsersFound;
|
||||||
final String chatCantBeDeleted;
|
final String chatCantBeDeleted;
|
||||||
|
final String chatProfileUsers;
|
||||||
|
|
||||||
/// Shown when the user has no name
|
/// Shown when the user has no name
|
||||||
final String anonymousUser;
|
final String anonymousUser;
|
|
@ -7,10 +7,10 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
import 'package:flutter_community_chat_view/src/components/chat_bottom.dart';
|
import 'package:flutter_chat_view/src/components/chat_bottom.dart';
|
||||||
import 'package:flutter_community_chat_view/src/components/chat_detail_row.dart';
|
import 'package:flutter_chat_view/src/components/chat_detail_row.dart';
|
||||||
import 'package:flutter_community_chat_view/src/components/image_loading_snackbar.dart';
|
import 'package:flutter_chat_view/src/components/image_loading_snackbar.dart';
|
||||||
|
|
||||||
class ChatDetailScreen extends StatefulWidget {
|
class ChatDetailScreen extends StatefulWidget {
|
||||||
const ChatDetailScreen({
|
const ChatDetailScreen({
|
||||||
|
@ -19,18 +19,16 @@ class ChatDetailScreen extends StatefulWidget {
|
||||||
required this.onUploadImage,
|
required this.onUploadImage,
|
||||||
required this.onReadChat,
|
required this.onReadChat,
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.chatUserService,
|
|
||||||
required this.messageService,
|
|
||||||
required this.pageSize,
|
required this.pageSize,
|
||||||
|
required this.chatId,
|
||||||
this.translations = const ChatTranslations(),
|
this.translations = const ChatTranslations(),
|
||||||
this.chat,
|
|
||||||
this.onPressChatTitle,
|
this.onPressChatTitle,
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
this.showTime = false,
|
this.showTime = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ChatModel? chat;
|
final String chatId;
|
||||||
|
|
||||||
/// The id of the current user that is viewing the chat.
|
/// The id of the current user that is viewing the chat.
|
||||||
|
|
||||||
|
@ -47,8 +45,6 @@ class ChatDetailScreen extends StatefulWidget {
|
||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
final bool showTime;
|
final bool showTime;
|
||||||
final ChatService service;
|
final ChatService service;
|
||||||
final ChatUserService chatUserService;
|
|
||||||
final MessageService messageService;
|
|
||||||
final int pageSize;
|
final int pageSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -60,36 +56,39 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
ChatUserModel? currentUser;
|
ChatUserModel? currentUser;
|
||||||
ScrollController controller = ScrollController();
|
ScrollController controller = ScrollController();
|
||||||
bool showIndicator = false;
|
bool showIndicator = false;
|
||||||
late MessageService messageSubscription;
|
late ChatDetailService messageSubscription;
|
||||||
Stream<List<ChatMessageModel>>? stream;
|
Stream<List<ChatMessageModel>>? stream;
|
||||||
ChatMessageModel? previousMessage;
|
ChatMessageModel? previousMessage;
|
||||||
List<Widget> detailRows = [];
|
List<Widget> detailRows = [];
|
||||||
|
ChatModel? chat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
messageSubscription = widget.messageService;
|
messageSubscription = widget.service.chatDetailService;
|
||||||
messageSubscription.addListener(onListen);
|
messageSubscription.addListener(onListen);
|
||||||
if (widget.chat != null) {
|
|
||||||
stream = widget.messageService.getMessagesStream(widget.chat!);
|
|
||||||
stream?.listen((event) {});
|
|
||||||
Future.delayed(Duration.zero, () async {
|
Future.delayed(Duration.zero, () async {
|
||||||
|
chat =
|
||||||
|
await widget.service.chatOverviewService.getChatById(widget.chatId);
|
||||||
|
|
||||||
if (detailRows.isEmpty) {
|
if (detailRows.isEmpty) {
|
||||||
await widget.messageService
|
await widget.service.chatDetailService.fetchMoreMessage(
|
||||||
.fetchMoreMessage(widget.pageSize, widget.chat!);
|
widget.pageSize,
|
||||||
|
chat!.id!,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
stream = widget.service.chatDetailService.getMessagesStream(chat!.id!);
|
||||||
|
stream?.listen((event) {});
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await widget.onReadChat(chat!);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (widget.chat != null) {
|
|
||||||
widget.onReadChat(widget.chat!);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onListen() {
|
void onListen() {
|
||||||
var chatMessages = [];
|
var chatMessages = [];
|
||||||
chatMessages = widget.messageService.getMessages();
|
chatMessages = widget.service.chatDetailService.getMessages();
|
||||||
detailRows = [];
|
detailRows = [];
|
||||||
previousMessage = null;
|
previousMessage = null;
|
||||||
for (var message in chatMessages) {
|
for (var message in chatMessages) {
|
||||||
|
@ -106,8 +105,11 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
}
|
}
|
||||||
detailRows = detailRows.reversed.toList();
|
detailRows = detailRows.reversed.toList();
|
||||||
|
|
||||||
widget.onReadChat(widget.chat!);
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await widget.onReadChat(chat!);
|
||||||
|
});
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +117,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
messageSubscription.removeListener(onListen);
|
messageSubscription.removeListener(onListen);
|
||||||
|
widget.service.chatDetailService.stopListeningForMessages();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,18 +135,19 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
var messenger = ScaffoldMessenger.of(context)
|
var messenger = ScaffoldMessenger.of(context)
|
||||||
..showSnackBar(
|
..showSnackBar(
|
||||||
getImageLoadingSnackbar(widget.translations),
|
getImageLoadingSnackbar(widget.translations),
|
||||||
);
|
)
|
||||||
|
..activate();
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
await widget.onUploadImage(image);
|
await widget.onUploadImage(image);
|
||||||
}
|
}
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
messenger.hideCurrentSnackBar();
|
messenger.hideCurrentSnackBar();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return FutureBuilder<ChatModel>(
|
return FutureBuilder<ChatModel>(
|
||||||
future: widget.service.getChatById(widget.chat?.id ?? ''),
|
future: widget.service.chatOverviewService.getChatById(widget.chatId),
|
||||||
builder: (context, AsyncSnapshot<ChatModel> snapshot) {
|
builder: (context, AsyncSnapshot<ChatModel> snapshot) {
|
||||||
var chatModel = snapshot.data;
|
var chatModel = snapshot.data;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -153,7 +157,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
onTap: () => widget.onPressChatTitle?.call(context, chatModel!),
|
onTap: () => widget.onPressChatTitle?.call(context, chatModel!),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: widget.chat == null
|
children: chat == null
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
if (chatModel is GroupChatModel) ...[
|
if (chatModel is GroupChatModel) ...[
|
||||||
|
@ -202,8 +206,8 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
setState(() {
|
setState(() {
|
||||||
showIndicator = true;
|
showIndicator = true;
|
||||||
});
|
});
|
||||||
await widget.messageService
|
await widget.service.chatDetailService
|
||||||
.fetchMoreMessage(widget.pageSize, widget.chat!);
|
.fetchMoreMessage(widget.pageSize, widget.chatId);
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
Future.delayed(const Duration(seconds: 2), () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
|
@ -0,0 +1,146 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
|
||||||
|
class ChatEntryWidget extends StatefulWidget {
|
||||||
|
const ChatEntryWidget({
|
||||||
|
required this.chatService,
|
||||||
|
required this.onTap,
|
||||||
|
this.widgetSize = 75,
|
||||||
|
this.backgroundColor = Colors.grey,
|
||||||
|
this.icon = Icons.chat,
|
||||||
|
this.iconColor = Colors.black,
|
||||||
|
this.counterBackgroundColor = Colors.red,
|
||||||
|
this.textStyle,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ChatService chatService;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final double widgetSize;
|
||||||
|
final Color counterBackgroundColor;
|
||||||
|
final Function() onTap;
|
||||||
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatEntryWidget> createState() => _ChatEntryWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatEntryWidgetState extends State<ChatEntryWidget> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => widget.onTap.call(),
|
||||||
|
child: StreamBuilder<int>(
|
||||||
|
stream:
|
||||||
|
widget.chatService.chatOverviewService.getUnreadChatsCountStream(),
|
||||||
|
builder: (BuildContext context, snapshot) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: widget.widgetSize,
|
||||||
|
height: widget.widgetSize,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: widget.backgroundColor,
|
||||||
|
),
|
||||||
|
child: _AnimatedNotificationIcon(
|
||||||
|
icon: Icon(
|
||||||
|
widget.icon,
|
||||||
|
color: widget.iconColor,
|
||||||
|
size: widget.widgetSize / 1.5,
|
||||||
|
),
|
||||||
|
notifications: snapshot.data ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 0.0,
|
||||||
|
top: 0.0,
|
||||||
|
child: Container(
|
||||||
|
width: widget.widgetSize / 2,
|
||||||
|
height: widget.widgetSize / 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: widget.counterBackgroundColor,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'${snapshot.data ?? 0}',
|
||||||
|
style: widget.textStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedNotificationIcon extends StatefulWidget {
|
||||||
|
const _AnimatedNotificationIcon({
|
||||||
|
required this.notifications,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int notifications;
|
||||||
|
final Icon icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AnimatedNotificationIcon> createState() =>
|
||||||
|
_AnimatedNotificationIconState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedNotificationIconState extends State<_AnimatedNotificationIcon>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _animationController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (widget.notifications != 0) {
|
||||||
|
unawaited(_runAnimation());
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_animationController.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _AnimatedNotificationIcon oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (oldWidget.notifications != widget.notifications) {
|
||||||
|
_runAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runAnimation() async {
|
||||||
|
await _animationController.forward();
|
||||||
|
await _animationController.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RotationTransition(
|
||||||
|
turns: Tween(begin: 0.0, end: -.1)
|
||||||
|
.chain(CurveTween(curve: Curves.elasticIn))
|
||||||
|
.animate(_animationController),
|
||||||
|
child: widget.icon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
import 'package:flutter_chat_view/src/services/profile_service.dart';
|
||||||
|
import 'package:flutter_profile/flutter_profile.dart';
|
||||||
|
|
||||||
|
class ChatProfileScreen extends StatefulWidget {
|
||||||
|
const ChatProfileScreen({
|
||||||
|
required this.chatService,
|
||||||
|
required this.chatId,
|
||||||
|
required this.translations,
|
||||||
|
required this.onTapUser,
|
||||||
|
this.userId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ChatTranslations translations;
|
||||||
|
final ChatService chatService;
|
||||||
|
final String chatId;
|
||||||
|
final String? userId;
|
||||||
|
final Function(String userId) onTapUser;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatProfileScreen> createState() => _ProfileScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileScreenState extends State<ChatProfileScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var size = MediaQuery.of(context).size;
|
||||||
|
var hasUser = widget.userId == null;
|
||||||
|
return FutureBuilder<dynamic>(
|
||||||
|
future: hasUser
|
||||||
|
? widget.chatService.chatOverviewService.getChatById(widget.chatId)
|
||||||
|
: widget.chatService.chatUserService.getUser(widget.userId!),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
var data = snapshot.data;
|
||||||
|
User? user;
|
||||||
|
|
||||||
|
if (data is ChatUserModel) {
|
||||||
|
user = User(
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
imageUrl: data.imageUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data is PersonalChatModel) {
|
||||||
|
user = User(
|
||||||
|
firstName: data.user.firstName,
|
||||||
|
lastName: data.user.lastName,
|
||||||
|
imageUrl: data.user.imageUrl,
|
||||||
|
);
|
||||||
|
} else if (data is GroupChatModel) {
|
||||||
|
user = User(
|
||||||
|
firstName: data.title,
|
||||||
|
imageUrl: data.imageUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
(data is ChatUserModel)
|
||||||
|
? '${data.firstName ?? ''} ${data.lastName ?? ''}'
|
||||||
|
: (data is PersonalChatModel)
|
||||||
|
? data.user.fullName ?? ''
|
||||||
|
: (data is GroupChatModel)
|
||||||
|
? data.title
|
||||||
|
: '',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: snapshot.hasData
|
||||||
|
? ListView(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
width: size.width,
|
||||||
|
child: ProfilePage(
|
||||||
|
user: user!,
|
||||||
|
service: ChatProfileService(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (data is GroupChatModel) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 100),
|
||||||
|
child: Text(
|
||||||
|
widget.translations.chatProfileUsers,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...data.users.map((e) {
|
||||||
|
var user = User(
|
||||||
|
firstName: e.firstName ?? '',
|
||||||
|
lastName: e.lastName ?? '',
|
||||||
|
imageUrl: e.imageUrl,
|
||||||
|
);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => widget.onTapUser.call(e.id!),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
user: user,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
user.firstName!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
312
packages/flutter_chat_view/lib/src/screens/chat_screen.dart
Normal file
312
packages/flutter_chat_view/lib/src/screens/chat_screen.dart
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
import 'package:flutter_chat_view/src/services/date_formatter.dart';
|
||||||
|
|
||||||
|
class ChatScreen extends StatefulWidget {
|
||||||
|
const ChatScreen({
|
||||||
|
required this.options,
|
||||||
|
required this.onPressStartChat,
|
||||||
|
required this.onPressChat,
|
||||||
|
required this.onDeleteChat,
|
||||||
|
required this.service,
|
||||||
|
this.onNoChats,
|
||||||
|
this.deleteChatDialog,
|
||||||
|
this.translations = const ChatTranslations(),
|
||||||
|
this.disableDismissForPermanentChats = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ChatOptions options;
|
||||||
|
final ChatTranslations translations;
|
||||||
|
final ChatService service;
|
||||||
|
final Function()? onPressStartChat;
|
||||||
|
final Function()? onNoChats;
|
||||||
|
final void Function(ChatModel chat) onDeleteChat;
|
||||||
|
final void Function(ChatModel chat) onPressChat;
|
||||||
|
|
||||||
|
/// Disable the swipe to dismiss feature for chats that are not deletable
|
||||||
|
final bool disableDismissForPermanentChats;
|
||||||
|
|
||||||
|
/// Method to optionally change the bottomsheetdialog
|
||||||
|
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
|
||||||
|
@override
|
||||||
|
State<ChatScreen> createState() => _ChatScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatScreenState extends State<ChatScreen> {
|
||||||
|
final DateFormatter _dateFormatter = DateFormatter();
|
||||||
|
bool _hasCalledOnNoChats = false;
|
||||||
|
ScrollController controller = ScrollController();
|
||||||
|
bool showIndicator = false;
|
||||||
|
Stream<List<ChatModel>>? chats;
|
||||||
|
List<String> deletedChats = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var translations = widget.translations;
|
||||||
|
return widget.options.scaffoldBuilder(
|
||||||
|
AppBar(
|
||||||
|
title: Text(translations.chatsTitle),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
StreamBuilder<int>(
|
||||||
|
stream:
|
||||||
|
widget.service.chatOverviewService.getUnreadChatsCountStream(),
|
||||||
|
builder: (BuildContext context, snapshot) => Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 22.0),
|
||||||
|
child: Text(
|
||||||
|
'${snapshot.data ?? 0} ${translations.chatsUnread}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFFBBBBBB),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
controller: controller,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.only(top: 15.0),
|
||||||
|
children: [
|
||||||
|
StreamBuilder<List<ChatModel>>(
|
||||||
|
stream: widget.service.chatOverviewService.getChatsStream(),
|
||||||
|
builder: (BuildContext context, snapshot) {
|
||||||
|
// if the stream is done, empty and noChats is set we should call that
|
||||||
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
|
(snapshot.data?.isEmpty ?? true)) {
|
||||||
|
if (widget.onNoChats != null && !_hasCalledOnNoChats) {
|
||||||
|
_hasCalledOnNoChats = true; // Set the flag to true
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await widget.onNoChats!.call();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_hasCalledOnNoChats =
|
||||||
|
false; // Reset the flag if there are chats
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
for (ChatModel chat in (snapshot.data ?? []).where(
|
||||||
|
(chat) => !deletedChats.contains(chat.id),
|
||||||
|
)) ...[
|
||||||
|
Builder(
|
||||||
|
builder: (context) => !(widget
|
||||||
|
.disableDismissForPermanentChats &&
|
||||||
|
!chat.canBeDeleted)
|
||||||
|
? Dismissible(
|
||||||
|
confirmDismiss: (_) async =>
|
||||||
|
widget.deleteChatDialog
|
||||||
|
?.call(context, chat) ??
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) =>
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
chat.canBeDeleted
|
||||||
|
? translations
|
||||||
|
.deleteChatModalTitle
|
||||||
|
: translations
|
||||||
|
.chatCantBeDeleted,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (chat.canBeDeleted)
|
||||||
|
Text(
|
||||||
|
translations
|
||||||
|
.deleteChatModalDescription,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
child: Text(
|
||||||
|
translations
|
||||||
|
.deleteChatModalCancel,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(false),
|
||||||
|
),
|
||||||
|
if (chat.canBeDeleted)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(true),
|
||||||
|
child: Text(
|
||||||
|
translations
|
||||||
|
.deleteChatModalConfirm,
|
||||||
|
style:
|
||||||
|
const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDismissed: (_) {
|
||||||
|
setState(() {
|
||||||
|
deletedChats.add(chat.id!);
|
||||||
|
});
|
||||||
|
widget.onDeleteChat(chat);
|
||||||
|
},
|
||||||
|
background: Container(
|
||||||
|
color: Colors.red,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
translations.deleteChatButton,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
key: ValueKey(
|
||||||
|
chat.id.toString(),
|
||||||
|
),
|
||||||
|
child: ChatListItem(
|
||||||
|
widget: widget,
|
||||||
|
chat: chat,
|
||||||
|
translations: translations,
|
||||||
|
dateFormatter: _dateFormatter,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ChatListItem(
|
||||||
|
widget: widget,
|
||||||
|
chat: chat,
|
||||||
|
translations: translations,
|
||||||
|
dateFormatter: _dateFormatter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onPressStartChat != null)
|
||||||
|
widget.options.newChatButtonBuilder(
|
||||||
|
context,
|
||||||
|
() async {
|
||||||
|
await widget.onPressStartChat!.call();
|
||||||
|
},
|
||||||
|
translations,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatListItem extends StatelessWidget {
|
||||||
|
const ChatListItem({
|
||||||
|
required this.widget,
|
||||||
|
required this.chat,
|
||||||
|
required this.translations,
|
||||||
|
required DateFormatter dateFormatter,
|
||||||
|
super.key,
|
||||||
|
}) : _dateFormatter = dateFormatter;
|
||||||
|
|
||||||
|
final ChatScreen widget;
|
||||||
|
final ChatModel chat;
|
||||||
|
final ChatTranslations translations;
|
||||||
|
final DateFormatter _dateFormatter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => widget.onPressChat(chat),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: widget.options.chatRowContainerBuilder(
|
||||||
|
(chat is PersonalChatModel)
|
||||||
|
? ChatRow(
|
||||||
|
unreadMessages: chat.unreadMessages ?? 0,
|
||||||
|
avatar: widget.options.userAvatarBuilder(
|
||||||
|
(chat as PersonalChatModel).user,
|
||||||
|
40.0,
|
||||||
|
),
|
||||||
|
title: (chat as PersonalChatModel).user.fullName ??
|
||||||
|
translations.anonymousUser,
|
||||||
|
subTitle: chat.lastMessage != null
|
||||||
|
? chat.lastMessage is ChatTextMessageModel
|
||||||
|
? (chat.lastMessage! as ChatTextMessageModel).text
|
||||||
|
: '📷 '
|
||||||
|
'${translations.image}'
|
||||||
|
: '',
|
||||||
|
lastUsed: chat.lastUsed != null
|
||||||
|
? _dateFormatter.format(
|
||||||
|
date: chat.lastUsed!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
: ChatRow(
|
||||||
|
title: (chat as GroupChatModel).title,
|
||||||
|
unreadMessages: chat.unreadMessages ?? 0,
|
||||||
|
subTitle: chat.lastMessage != null
|
||||||
|
? chat.lastMessage is ChatTextMessageModel
|
||||||
|
? (chat.lastMessage! as ChatTextMessageModel).text
|
||||||
|
: '📷 '
|
||||||
|
'${translations.image}'
|
||||||
|
: '',
|
||||||
|
avatar: widget.options.groupAvatarBuilder(
|
||||||
|
(chat as GroupChatModel).title,
|
||||||
|
(chat as GroupChatModel).imageUrl,
|
||||||
|
40.0,
|
||||||
|
),
|
||||||
|
lastUsed: chat.lastUsed != null
|
||||||
|
? _dateFormatter.format(
|
||||||
|
date: chat.lastUsed!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,13 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
import 'package:flutter_chat_view/flutter_chat_view.dart';
|
||||||
|
|
||||||
class NewChatScreen extends StatefulWidget {
|
class NewChatScreen extends StatefulWidget {
|
||||||
const NewChatScreen({
|
const NewChatScreen({
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.onPressCreateChat,
|
required this.onPressCreateChat,
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.userService,
|
|
||||||
this.translations = const ChatTranslations(),
|
this.translations = const ChatTranslations(),
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
@ -18,7 +17,6 @@ class NewChatScreen extends StatefulWidget {
|
||||||
final ChatOptions options;
|
final ChatOptions options;
|
||||||
final ChatTranslations translations;
|
final ChatTranslations translations;
|
||||||
final ChatService service;
|
final ChatService service;
|
||||||
final ChatUserService userService;
|
|
||||||
final Function(ChatUserModel) onPressCreateChat;
|
final Function(ChatUserModel) onPressCreateChat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -40,7 +38,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: FutureBuilder<List<ChatUserModel>>(
|
body: FutureBuilder<List<ChatUserModel>>(
|
||||||
future: widget.userService.getAllUsers(),
|
future: widget.service.chatUserService.getAllUsers(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
|
@ -0,0 +1,24 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_profile/flutter_profile.dart';
|
||||||
|
|
||||||
|
class ChatProfileService extends ProfileService {
|
||||||
|
@override
|
||||||
|
FutureOr<void> editProfile(User user, String key, String? value) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> pageBottomAction() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> uploadImage(
|
||||||
|
BuildContext context, {
|
||||||
|
required Function(bool isUploading) onUploadStateChanged,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
name: flutter_community_chat_view
|
name: flutter_chat_view
|
||||||
description: A standard flutter package.
|
description: A standard flutter package.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
|
@ -16,16 +16,20 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
intl: any
|
intl: any
|
||||||
flutter_community_chat_interface:
|
flutter_chat_interface:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat
|
url: https://github.com/Iconica-Development/flutter_chat
|
||||||
path: packages/flutter_community_chat_interface
|
path: packages/flutter_chat_interface
|
||||||
ref: 1.0.0
|
ref: 1.0.0
|
||||||
cached_network_image: ^3.2.2
|
cached_network_image: ^3.2.2
|
||||||
flutter_image_picker:
|
flutter_image_picker:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_image_picker
|
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||||
ref: 1.0.4
|
ref: 1.0.4
|
||||||
|
flutter_profile:
|
||||||
|
git:
|
||||||
|
ref: 1.1.5
|
||||||
|
url: https://github.com/Iconica-Development/flutter_profile
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
|
@ -1,11 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
library flutter_community_chat;
|
|
||||||
|
|
||||||
export 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
|
||||||
export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
|
||||||
export 'package:flutter_community_chat/src/routes.dart';
|
|
||||||
export 'package:flutter_community_chat/src/models/community_chat_configuration.dart';
|
|
||||||
export 'package:flutter_community_chat/src/flutter_community_chat_userstory.dart';
|
|
|
@ -1,7 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
library flutter_community_chat_firebase;
|
|
||||||
|
|
||||||
export 'package:flutter_community_chat_firebase/service/service.dart';
|
|
|
@ -1,3 +0,0 @@
|
||||||
export 'package:flutter_community_chat_firebase/service/firebase_user_service.dart';
|
|
||||||
export 'package:flutter_community_chat_firebase/service/firebase_message_service.dart';
|
|
||||||
export 'package:flutter_community_chat_firebase/service/firebase_chat_service.dart';
|
|
|
@ -1,9 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
library flutter_community_chat_interface;
|
|
||||||
|
|
||||||
export 'package:flutter_community_chat_interface/src/chat_data_provider.dart';
|
|
||||||
export 'package:flutter_community_chat_interface/src/model/model.dart';
|
|
||||||
export 'package:flutter_community_chat_interface/src/service/service.dart';
|
|
|
@ -1,23 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter_community_chat_interface/src/model/chat_message.dart';
|
|
||||||
|
|
||||||
abstract class ChatModel {
|
|
||||||
ChatModel({
|
|
||||||
this.id,
|
|
||||||
this.messages = const [],
|
|
||||||
this.unreadMessages,
|
|
||||||
this.lastUsed,
|
|
||||||
this.lastMessage,
|
|
||||||
this.canBeDeleted = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
String? id;
|
|
||||||
List<ChatMessageModel>? messages;
|
|
||||||
int? unreadMessages;
|
|
||||||
DateTime? lastUsed;
|
|
||||||
ChatMessageModel? lastMessage;
|
|
||||||
bool canBeDeleted;
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter_community_chat_interface/src/model/chat_message.dart';
|
|
||||||
|
|
||||||
class ChatImageMessageModel extends ChatMessageModel {
|
|
||||||
ChatImageMessageModel({
|
|
||||||
required super.sender,
|
|
||||||
required super.timestamp,
|
|
||||||
required this.imageUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String imageUrl;
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter_community_chat_interface/src/model/chat_user.dart';
|
|
||||||
|
|
||||||
abstract class ChatMessageModel {
|
|
||||||
const ChatMessageModel({
|
|
||||||
required this.sender,
|
|
||||||
required this.timestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ChatUserModel sender;
|
|
||||||
final DateTime timestamp;
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter_community_chat_interface/src/model/chat_message.dart';
|
|
||||||
|
|
||||||
class ChatTextMessageModel extends ChatMessageModel {
|
|
||||||
ChatTextMessageModel({
|
|
||||||
required super.sender,
|
|
||||||
required super.timestamp,
|
|
||||||
required this.text,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String text;
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
|
||||||
|
|
||||||
class GroupChatModel extends ChatModel {
|
|
||||||
GroupChatModel({
|
|
||||||
required this.title,
|
|
||||||
required this.imageUrl,
|
|
||||||
required this.users,
|
|
||||||
super.id,
|
|
||||||
super.messages,
|
|
||||||
super.lastUsed,
|
|
||||||
super.lastMessage,
|
|
||||||
super.unreadMessages,
|
|
||||||
super.canBeDeleted,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final String imageUrl;
|
|
||||||
final List<ChatUserModel> users;
|
|
||||||
|
|
||||||
GroupChatModel copyWith({
|
|
||||||
String? id,
|
|
||||||
List<ChatMessageModel>? messages,
|
|
||||||
int? unreadMessages,
|
|
||||||
DateTime? lastUsed,
|
|
||||||
ChatMessageModel? lastMessage,
|
|
||||||
String? title,
|
|
||||||
String? imageUrl,
|
|
||||||
List<ChatUserModel>? users,
|
|
||||||
bool? canBeDeleted,
|
|
||||||
}) =>
|
|
||||||
GroupChatModel(
|
|
||||||
id: id ?? this.id,
|
|
||||||
messages: messages ?? this.messages,
|
|
||||||
unreadMessages: unreadMessages ?? this.unreadMessages,
|
|
||||||
lastUsed: lastUsed ?? this.lastUsed,
|
|
||||||
lastMessage: lastMessage ?? this.lastMessage,
|
|
||||||
title: title ?? this.title,
|
|
||||||
imageUrl: imageUrl ?? this.imageUrl,
|
|
||||||
users: users ?? this.users,
|
|
||||||
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
|
||||||
|
|
||||||
class PersonalChatModel extends ChatModel {
|
|
||||||
PersonalChatModel({
|
|
||||||
required this.user,
|
|
||||||
super.id,
|
|
||||||
super.messages,
|
|
||||||
super.unreadMessages,
|
|
||||||
super.lastUsed,
|
|
||||||
super.lastMessage,
|
|
||||||
super.canBeDeleted,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ChatUserModel user;
|
|
||||||
|
|
||||||
PersonalChatModel copyWith({
|
|
||||||
String? id,
|
|
||||||
List<ChatMessageModel>? messages,
|
|
||||||
int? unreadMessages,
|
|
||||||
DateTime? lastUsed,
|
|
||||||
ChatMessageModel? lastMessage,
|
|
||||||
ChatUserModel? user,
|
|
||||||
bool? canBeDeleted,
|
|
||||||
}) =>
|
|
||||||
PersonalChatModel(
|
|
||||||
id: id ?? this.id,
|
|
||||||
messages: messages ?? this.messages,
|
|
||||||
unreadMessages: unreadMessages ?? this.unreadMessages,
|
|
||||||
lastUsed: lastUsed ?? this.lastUsed,
|
|
||||||
lastMessage: lastMessage ?? this.lastMessage,
|
|
||||||
user: user ?? this.user,
|
|
||||||
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export 'chat_service.dart';
|
|
||||||
export 'user_service.dart';
|
|
||||||
export 'message_service.dart';
|
|
|
@ -1,367 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
|
||||||
import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
|
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
|
||||||
const ChatScreen({
|
|
||||||
required this.options,
|
|
||||||
required this.onPressStartChat,
|
|
||||||
required this.onPressChat,
|
|
||||||
required this.onDeleteChat,
|
|
||||||
required this.service,
|
|
||||||
required this.pageSize,
|
|
||||||
this.onNoChats,
|
|
||||||
this.deleteChatDialog,
|
|
||||||
this.translations = const ChatTranslations(),
|
|
||||||
this.disableDismissForPermanentChats = false,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ChatOptions options;
|
|
||||||
final ChatTranslations translations;
|
|
||||||
final ChatService service;
|
|
||||||
final Function()? onPressStartChat;
|
|
||||||
final Function()? onNoChats;
|
|
||||||
final void Function(ChatModel chat) onDeleteChat;
|
|
||||||
final void Function(ChatModel chat) onPressChat;
|
|
||||||
final int pageSize;
|
|
||||||
|
|
||||||
/// Disable the swipe to dismiss feature for chats that are not deletable
|
|
||||||
final bool disableDismissForPermanentChats;
|
|
||||||
|
|
||||||
/// Method to optionally change the bottomsheetdialog
|
|
||||||
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
|
|
||||||
@override
|
|
||||||
State<ChatScreen> createState() => _ChatScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatScreenState extends State<ChatScreen> {
|
|
||||||
final DateFormatter _dateFormatter = DateFormatter();
|
|
||||||
bool _hasCalledOnNoChats = false;
|
|
||||||
ScrollController controller = ScrollController();
|
|
||||||
bool showIndicator = false;
|
|
||||||
Stream<List<ChatModel>>? chats;
|
|
||||||
List<String> deletedChats = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
getChats();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void getChats() {
|
|
||||||
setState(() {
|
|
||||||
chats = widget.service.getChatsStream(widget.pageSize);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var translations = widget.translations;
|
|
||||||
return widget.options.scaffoldBuilder(
|
|
||||||
AppBar(
|
|
||||||
title: Text(translations.chatsTitle),
|
|
||||||
centerTitle: true,
|
|
||||||
actions: [
|
|
||||||
StreamBuilder<int>(
|
|
||||||
stream: widget.service.getUnreadChatsCountStream(),
|
|
||||||
builder: (BuildContext context, snapshot) => Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 22.0),
|
|
||||||
child: Text(
|
|
||||||
'${snapshot.data ?? 0} ${translations.chatsUnread}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Color(0xFFBBBBBB),
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Listener(
|
|
||||||
onPointerMove: (event) {
|
|
||||||
var isTop = controller.position.pixels ==
|
|
||||||
controller.position.maxScrollExtent;
|
|
||||||
|
|
||||||
if (showIndicator == false &&
|
|
||||||
!isTop &&
|
|
||||||
controller.position.userScrollDirection ==
|
|
||||||
ScrollDirection.reverse) {
|
|
||||||
setState(() {
|
|
||||||
showIndicator = true;
|
|
||||||
});
|
|
||||||
getChats();
|
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
showIndicator = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: ListView(
|
|
||||||
controller: controller,
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
padding: const EdgeInsets.only(top: 15.0),
|
|
||||||
children: [
|
|
||||||
StreamBuilder<List<ChatModel>>(
|
|
||||||
stream: chats,
|
|
||||||
builder: (BuildContext context, snapshot) {
|
|
||||||
// if the stream is done, empty and noChats is set we should call that
|
|
||||||
if (snapshot.connectionState == ConnectionState.done &&
|
|
||||||
(snapshot.data?.isEmpty ?? true)) {
|
|
||||||
if (widget.onNoChats != null && !_hasCalledOnNoChats) {
|
|
||||||
_hasCalledOnNoChats = true; // Set the flag to true
|
|
||||||
WidgetsBinding.instance
|
|
||||||
.addPostFrameCallback((_) async {
|
|
||||||
await widget.onNoChats!.call();
|
|
||||||
getChats();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_hasCalledOnNoChats =
|
|
||||||
false; // Reset the flag if there are chats
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
for (ChatModel chat in (snapshot.data ?? []).where(
|
|
||||||
(chat) => !deletedChats.contains(chat.id),
|
|
||||||
)) ...[
|
|
||||||
Builder(
|
|
||||||
builder: (context) => !(widget
|
|
||||||
.disableDismissForPermanentChats &&
|
|
||||||
!chat.canBeDeleted)
|
|
||||||
? Dismissible(
|
|
||||||
confirmDismiss: (_) async =>
|
|
||||||
widget.deleteChatDialog
|
|
||||||
?.call(context, chat) ??
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) =>
|
|
||||||
Container(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
chat.canBeDeleted
|
|
||||||
? translations
|
|
||||||
.deleteChatModalTitle
|
|
||||||
: translations
|
|
||||||
.chatCantBeDeleted,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight:
|
|
||||||
FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (chat.canBeDeleted)
|
|
||||||
Text(
|
|
||||||
translations
|
|
||||||
.deleteChatModalDescription,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment
|
|
||||||
.center,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
child: Text(
|
|
||||||
translations
|
|
||||||
.deleteChatModalCancel,
|
|
||||||
style:
|
|
||||||
const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () =>
|
|
||||||
Navigator.of(
|
|
||||||
context,
|
|
||||||
).pop(false),
|
|
||||||
),
|
|
||||||
if (chat.canBeDeleted)
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () =>
|
|
||||||
Navigator.of(
|
|
||||||
context,
|
|
||||||
).pop(true),
|
|
||||||
child: Text(
|
|
||||||
translations
|
|
||||||
.deleteChatModalConfirm,
|
|
||||||
style:
|
|
||||||
const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onDismissed: (_) {
|
|
||||||
setState(() {
|
|
||||||
deletedChats.add(chat.id!);
|
|
||||||
});
|
|
||||||
widget.onDeleteChat(chat);
|
|
||||||
},
|
|
||||||
background: Container(
|
|
||||||
color: Colors.red,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
translations.deleteChatButton,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
key: ValueKey(
|
|
||||||
chat.id.toString(),
|
|
||||||
),
|
|
||||||
child: ChatListItem(
|
|
||||||
widget: widget,
|
|
||||||
chat: chat,
|
|
||||||
translations: translations,
|
|
||||||
dateFormatter: _dateFormatter,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ChatListItem(
|
|
||||||
widget: widget,
|
|
||||||
chat: chat,
|
|
||||||
translations: translations,
|
|
||||||
dateFormatter: _dateFormatter,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (showIndicator &&
|
|
||||||
snapshot.connectionState !=
|
|
||||||
ConnectionState.done) ...[
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
const CircularProgressIndicator(),
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.onPressStartChat != null)
|
|
||||||
widget.options.newChatButtonBuilder(
|
|
||||||
context,
|
|
||||||
() async {
|
|
||||||
await widget.onPressStartChat!.call();
|
|
||||||
getChats();
|
|
||||||
},
|
|
||||||
translations,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatListItem extends StatelessWidget {
|
|
||||||
const ChatListItem({
|
|
||||||
required this.widget,
|
|
||||||
required this.chat,
|
|
||||||
required this.translations,
|
|
||||||
required DateFormatter dateFormatter,
|
|
||||||
super.key,
|
|
||||||
}) : _dateFormatter = dateFormatter;
|
|
||||||
|
|
||||||
final ChatScreen widget;
|
|
||||||
final ChatModel chat;
|
|
||||||
final ChatTranslations translations;
|
|
||||||
final DateFormatter _dateFormatter;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => widget.onPressChat(chat),
|
|
||||||
child: Container(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: widget.options.chatRowContainerBuilder(
|
|
||||||
(chat is PersonalChatModel)
|
|
||||||
? ChatRow(
|
|
||||||
unreadMessages: chat.unreadMessages ?? 0,
|
|
||||||
avatar: widget.options.userAvatarBuilder(
|
|
||||||
(chat as PersonalChatModel).user,
|
|
||||||
40.0,
|
|
||||||
),
|
|
||||||
title: (chat as PersonalChatModel).user.fullName ??
|
|
||||||
translations.anonymousUser,
|
|
||||||
subTitle: chat.lastMessage != null
|
|
||||||
? chat.lastMessage is ChatTextMessageModel
|
|
||||||
? (chat.lastMessage! as ChatTextMessageModel).text
|
|
||||||
: '📷 '
|
|
||||||
'${translations.image}'
|
|
||||||
: '',
|
|
||||||
lastUsed: chat.lastUsed != null
|
|
||||||
? _dateFormatter.format(
|
|
||||||
date: chat.lastUsed!,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
)
|
|
||||||
: ChatRow(
|
|
||||||
title: (chat as GroupChatModel).title,
|
|
||||||
unreadMessages: chat.unreadMessages ?? 0,
|
|
||||||
subTitle: chat.lastMessage != null
|
|
||||||
? chat.lastMessage is ChatTextMessageModel
|
|
||||||
? (chat.lastMessage! as ChatTextMessageModel).text
|
|
||||||
: '📷 '
|
|
||||||
'${translations.image}'
|
|
||||||
: '',
|
|
||||||
avatar: widget.options.groupAvatarBuilder(
|
|
||||||
(chat as GroupChatModel).title,
|
|
||||||
(chat as GroupChatModel).imageUrl,
|
|
||||||
40.0,
|
|
||||||
),
|
|
||||||
lastUsed: chat.lastUsed != null
|
|
||||||
? _dateFormatter.format(
|
|
||||||
date: chat.lastUsed!,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: flutter_community_chat_workspace
|
name: flutter_chat_workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.1.0 <4.0.0'
|
sdk: ">=3.1.0 <4.0.0"
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
melos: ^3.0.1
|
melos: ^3.0.1
|
||||||
|
|
Loading…
Reference in a new issue