mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-19 10:53:51 +02:00
commit
f8ca89a762
23 changed files with 943 additions and 320 deletions
88
README.md
88
README.md
|
@ -1,4 +1,5 @@
|
||||||
# Flutter Community Chat
|
# Flutter Community 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 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.
|
||||||
|
|
||||||

|

|
||||||
|
@ -7,6 +8,7 @@ Figma Design that defines this component (only accessible for Iconica developers
|
||||||
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_community_chat as a dependency in your pubspec.yaml file:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -25,25 +27,84 @@ If you are going to use Firebase as the back-end of the Community Chat, you shou
|
||||||
path: packages/flutter_community_chat_firebase
|
path: packages/flutter_community_chat_firebase
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Create a Firebase project for your application and add firebase firestore and storage.
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
To use the module within your Flutter-application you should add the following code to the build-method of a chosen widget.
|
|
||||||
|
To use the module within your Flutter-application with predefined `Go_router` routes you should add the following:
|
||||||
|
|
||||||
|
Add go_router as dependency to your project.
|
||||||
|
Add the following configuration to your flutter_application:
|
||||||
|
|
||||||
```
|
```
|
||||||
CommunityChat(
|
List<GoRoute> getCommunityChatRoutes() => getCommunityChatStoryRoutes(
|
||||||
dataProvider: FirebaseCommunityChatDataProvider(),
|
CommunityChatUserStoryConfiguration(
|
||||||
)
|
service: FirebaseChatService(userService: FirebaseUserService()),
|
||||||
|
userService: FirebaseUserService(),
|
||||||
|
messageService:
|
||||||
|
FirebaseMessageService(userService: FirebaseUserService()),
|
||||||
|
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
||||||
|
),
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example we provide a `FirebaseCommunityChatDataProvider` as a data provider. You can also specify your own implementation here of the `CommunityChatInterface` interface.
|
Add the `getCommunityChatRoutes()` to your go_router routes like so:
|
||||||
|
|
||||||
You can also include your custom configuration for both the Community Chat itself as the Image Picker which is included in this package. You can specify those configurations as a parameter:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
CommunityChat(
|
final GoRouter _router = GoRouter(
|
||||||
dataProvider: FirebaseCommunityChatDataProvider(),
|
routes: <RouteBase>[
|
||||||
imagePickerTheme: ImagePickerTheme(),
|
GoRoute(
|
||||||
chatOptions: ChatOptions(),
|
path: '/',
|
||||||
)
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
|
return const MyHomePage(
|
||||||
|
title: "home",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
...getCommunityChatRoutes()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
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 add the `ChatScreen` add the following code:
|
||||||
|
|
||||||
|
````
|
||||||
|
ChatScreen(
|
||||||
|
options: options,
|
||||||
|
onPressStartChat: onPressStartChat,
|
||||||
|
onPressChat: onPressChat,
|
||||||
|
onDeleteChat: onDeleteChat,
|
||||||
|
service: service,
|
||||||
|
pageSize: pageSize,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
To add the `ChatDetailScreen` add the following code:
|
||||||
|
|
||||||
|
```
|
||||||
|
ChatDetailScreen(
|
||||||
|
options: options,
|
||||||
|
onMessageSubmit: onMessageSubmit,
|
||||||
|
onUploadImage: onUploadImage,
|
||||||
|
onReadChat: onReadChat,
|
||||||
|
service: service,
|
||||||
|
chatUserService: chatUserService,
|
||||||
|
messageService: messageService,
|
||||||
|
pageSize: pageSize,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
To add the `NewChatScreen` add the following code:
|
||||||
|
|
||||||
|
```
|
||||||
|
NewChatScreen(
|
||||||
|
options: options,
|
||||||
|
onPressCreateChat: onPressCreateChat,
|
||||||
|
service: service,
|
||||||
|
userService: userService,
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
The `ChatOptions` has its own parameters, as specified below:
|
The `ChatOptions` has its own parameters, as specified below:
|
||||||
|
@ -51,7 +112,7 @@ The `ChatOptions` has its own parameters, as specified below:
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| newChatButtonBuilder | Builds the 'New Chat' button, to initiate a new chat session. This button is displayed on the chat overview. |
|
| newChatButtonBuilder | Builds the 'New Chat' button, to initiate a new chat session. This button is displayed on the chat overview. |
|
||||||
| messageInputBuilder | Builds the text input which is displayed within the chat view, responsible for sending text messages. |
|
| messageInputBuilder | Builds the text input which is displayed within the chat view, responsible for sending text messages. |
|
||||||
| chatRowContainerBuilder | Builds a chat row. A row with the users' avatar, name and eventually the last massage sended in the chat. This builder is used both in the *chat overview screen* as in the *new chat screen*. |
|
| chatRowContainerBuilder | Builds a chat row. A row with the users' avatar, name and eventually the last massage sended in the chat. This builder is used both in the _chat overview screen_ as in the _new chat screen_. |
|
||||||
| imagePickerContainerBuilder | Builds the container around the ImagePicker. |
|
| imagePickerContainerBuilder | Builds the container around the ImagePicker. |
|
||||||
| closeImagePickerButtonBuilder | Builds the close button for the Image Picker pop-up window. |
|
| closeImagePickerButtonBuilder | Builds the close button for the Image Picker pop-up window. |
|
||||||
| scaffoldBuilder | Builds the default Scaffold-widget around the Community Chat. The chat title is displayed within the Scaffolds' title for example. |
|
| scaffoldBuilder | Builds the default Scaffold-widget around the Community Chat. The chat title is displayed within the Scaffolds' title for example. |
|
||||||
|
@ -69,3 +130,4 @@ If you would like to contribute to the plugin (e.g. by improving the documentati
|
||||||
## 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_community_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
|
||||||
|
````
|
||||||
|
|
|
@ -17,19 +17,26 @@ List<GoRoute> getCommunityChatStoryRoutes(
|
||||||
path: CommunityChatUserStoryRoutes.chatScreen,
|
path: CommunityChatUserStoryRoutes.chatScreen,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
var chatScreen = ChatScreen(
|
var chatScreen = ChatScreen(
|
||||||
|
pageSize: configuration.pageSize,
|
||||||
service: configuration.service,
|
service: configuration.service,
|
||||||
options: configuration.chatOptionsBuilder(context),
|
options: configuration.chatOptionsBuilder(context),
|
||||||
onNoChats: () =>
|
onNoChats: () async =>
|
||||||
context.push(CommunityChatUserStoryRoutes.newChatScreen),
|
await context.push(CommunityChatUserStoryRoutes.newChatScreen),
|
||||||
onPressStartChat: () =>
|
onPressStartChat: () async {
|
||||||
configuration.onPressStartChat?.call() ??
|
if (configuration.onPressStartChat != null) {
|
||||||
context.push(CommunityChatUserStoryRoutes.newChatScreen),
|
return await configuration.onPressStartChat?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await context
|
||||||
|
.push(CommunityChatUserStoryRoutes.newChatScreen);
|
||||||
|
},
|
||||||
onPressChat: (chat) =>
|
onPressChat: (chat) =>
|
||||||
configuration.onPressChat?.call(context, chat) ??
|
configuration.onPressChat?.call(context, chat) ??
|
||||||
context.push(
|
context.push(
|
||||||
CommunityChatUserStoryRoutes.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),
|
||||||
deleteChatDialog: configuration.deleteChatDialog,
|
deleteChatDialog: configuration.deleteChatDialog,
|
||||||
translations: configuration.translations,
|
translations: configuration.translations,
|
||||||
);
|
);
|
||||||
|
@ -52,6 +59,7 @@ List<GoRoute> getCommunityChatStoryRoutes(
|
||||||
var chatId = state.pathParameters['id'];
|
var chatId = state.pathParameters['id'];
|
||||||
var chat = PersonalChatModel(user: ChatUserModel(), id: chatId);
|
var chat = PersonalChatModel(user: ChatUserModel(), id: chatId);
|
||||||
var chatDetailScreen = ChatDetailScreen(
|
var chatDetailScreen = ChatDetailScreen(
|
||||||
|
pageSize: configuration.messagePageSize,
|
||||||
options: configuration.chatOptionsBuilder(context),
|
options: configuration.chatOptionsBuilder(context),
|
||||||
translations: configuration.translations,
|
translations: configuration.translations,
|
||||||
chatUserService: configuration.userService,
|
chatUserService: configuration.userService,
|
||||||
|
@ -110,7 +118,8 @@ List<GoRoute> getCommunityChatStoryRoutes(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.push(CommunityChatUserStoryRoutes.chatDetailViewPath(
|
await context.push(
|
||||||
|
CommunityChatUserStoryRoutes.chatDetailViewPath(
|
||||||
chat.id ?? ''));
|
chat.id ?? ''));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ class CommunityChatUserStoryConfiguration {
|
||||||
required this.messageService,
|
required this.messageService,
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.chatOptionsBuilder,
|
required this.chatOptionsBuilder,
|
||||||
|
this.pageSize = 10,
|
||||||
this.onPressStartChat,
|
this.onPressStartChat,
|
||||||
this.onPressChat,
|
this.onPressChat,
|
||||||
this.onDeleteChat,
|
this.onDeleteChat,
|
||||||
|
@ -29,6 +30,7 @@ class CommunityChatUserStoryConfiguration {
|
||||||
this.chatPageBuilder,
|
this.chatPageBuilder,
|
||||||
this.onPressChatTitle,
|
this.onPressChatTitle,
|
||||||
this.afterMessageSent,
|
this.afterMessageSent,
|
||||||
|
this.messagePageSize = 20,
|
||||||
});
|
});
|
||||||
final ChatService service;
|
final ChatService service;
|
||||||
final ChatUserService userService;
|
final ChatUserService userService;
|
||||||
|
@ -48,6 +50,8 @@ class CommunityChatUserStoryConfiguration {
|
||||||
|
|
||||||
/// If true, the user will be routed to the new chat screen if there are no chats.
|
/// If true, the user will be routed to the new chat screen if there are no chats.
|
||||||
final bool routeToNewChatIfEmpty;
|
final bool routeToNewChatIfEmpty;
|
||||||
|
final int pageSize;
|
||||||
|
final int messagePageSize;
|
||||||
|
|
||||||
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
|
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
|
||||||
final Function(BuildContext context, ChatModel chat)? onPressChatTitle;
|
final Function(BuildContext context, ChatModel chat)? onPressChatTitle;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
name: flutter_community_chat
|
name: flutter_community_chat
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 0.6.0
|
version: 1.0.0
|
||||||
|
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
|
@ -15,17 +15,17 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
go_router: ^12.1.1
|
go_router: any
|
||||||
flutter_community_chat_view:
|
flutter_community_chat_view:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat
|
url: https://github.com/Iconica-Development/flutter_community_chat
|
||||||
path: packages/flutter_community_chat_view
|
path: packages/flutter_community_chat_view
|
||||||
ref: 0.6.0
|
ref: 1.0.0
|
||||||
flutter_community_chat_interface:
|
flutter_community_chat_interface:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat
|
url: https://github.com/Iconica-Development/flutter_community_chat
|
||||||
path: packages/flutter_community_chat_interface
|
path: packages/flutter_community_chat_interface
|
||||||
ref: 0.6.0
|
ref: 1.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
|
|
|
@ -11,18 +11,26 @@ class FirebaseChatOptions {
|
||||||
this.chatsCollectionName = 'chats',
|
this.chatsCollectionName = 'chats',
|
||||||
this.messagesCollectionName = 'messages',
|
this.messagesCollectionName = 'messages',
|
||||||
this.usersCollectionName = 'users',
|
this.usersCollectionName = 'users',
|
||||||
|
this.chatsMetaDataCollectionName = 'chat_metadata',
|
||||||
|
this.userChatsCollectionName = 'chats',
|
||||||
});
|
});
|
||||||
|
|
||||||
final String groupChatsCollectionName;
|
final String groupChatsCollectionName;
|
||||||
final String chatsCollectionName;
|
final String chatsCollectionName;
|
||||||
final String messagesCollectionName;
|
final String messagesCollectionName;
|
||||||
final String usersCollectionName;
|
final String usersCollectionName;
|
||||||
|
final String chatsMetaDataCollectionName;
|
||||||
|
|
||||||
|
///This is the collection inside the user document.
|
||||||
|
final String userChatsCollectionName;
|
||||||
|
|
||||||
FirebaseChatOptions copyWith({
|
FirebaseChatOptions copyWith({
|
||||||
String? groupChatsCollectionName,
|
String? groupChatsCollectionName,
|
||||||
String? chatsCollectionName,
|
String? chatsCollectionName,
|
||||||
String? messagesCollectionName,
|
String? messagesCollectionName,
|
||||||
String? usersCollectionName,
|
String? usersCollectionName,
|
||||||
|
String? chatsMetaDataCollectionName,
|
||||||
|
String? userChatsCollectionName,
|
||||||
}) {
|
}) {
|
||||||
return FirebaseChatOptions(
|
return FirebaseChatOptions(
|
||||||
groupChatsCollectionName:
|
groupChatsCollectionName:
|
||||||
|
@ -31,6 +39,10 @@ class FirebaseChatOptions {
|
||||||
messagesCollectionName:
|
messagesCollectionName:
|
||||||
messagesCollectionName ?? this.messagesCollectionName,
|
messagesCollectionName ?? this.messagesCollectionName,
|
||||||
usersCollectionName: usersCollectionName ?? this.usersCollectionName,
|
usersCollectionName: usersCollectionName ?? this.usersCollectionName,
|
||||||
|
chatsMetaDataCollectionName:
|
||||||
|
chatsMetaDataCollectionName ?? this.chatsMetaDataCollectionName,
|
||||||
|
userChatsCollectionName:
|
||||||
|
userChatsCollectionName ?? this.userChatsCollectionName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ class FirebaseChatService implements ChatService {
|
||||||
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({
|
FirebaseChatService({
|
||||||
required ChatUserService userService,
|
required ChatUserService userService,
|
||||||
|
@ -37,7 +41,7 @@ class FirebaseChatService implements ChatService {
|
||||||
var snapshots = _db
|
var snapshots = _db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection('chats')
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(chatId)
|
.doc(chatId)
|
||||||
.snapshots();
|
.snapshots();
|
||||||
|
|
||||||
|
@ -52,7 +56,7 @@ class FirebaseChatService implements ChatService {
|
||||||
Function(List<ChatModel>) onReceivedChats,
|
Function(List<ChatModel>) onReceivedChats,
|
||||||
) {
|
) {
|
||||||
var snapshots = _db
|
var snapshots = _db
|
||||||
.collection(_options.chatsCollectionName)
|
.collection(_options.chatsMetaDataCollectionName)
|
||||||
.where(
|
.where(
|
||||||
FieldPath.documentId,
|
FieldPath.documentId,
|
||||||
whereIn: chatIds,
|
whereIn: chatIds,
|
||||||
|
@ -228,19 +232,29 @@ class FirebaseChatService implements ChatService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<ChatModel>> getChatsStream() {
|
Stream<List<ChatModel>> getChatsStream(int pageSize) {
|
||||||
late StreamController<List<ChatModel>> controller;
|
late StreamController<List<ChatModel>> controller;
|
||||||
StreamSubscription? chatsSubscription;
|
StreamSubscription? chatsSubscription;
|
||||||
controller = StreamController(
|
controller = StreamController(
|
||||||
onListen: () async {
|
onListen: () async {
|
||||||
|
QuerySnapshot<Map<String, dynamic>> userSnapshot;
|
||||||
|
List<String> userChatIds;
|
||||||
var currentUser = await _userService.getCurrentUser();
|
var currentUser = await _userService.getCurrentUser();
|
||||||
|
|
||||||
var userSnapshot = await _db
|
var userQuery = _db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(currentUser?.id)
|
.doc(currentUser?.id)
|
||||||
.collection('chats')
|
.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();
|
.get();
|
||||||
var userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
|
userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
|
||||||
|
}
|
||||||
|
|
||||||
var userGroupChatIds = await _db
|
var userGroupChatIds = await _db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
|
@ -248,10 +262,30 @@ class FirebaseChatService implements ChatService {
|
||||||
.get()
|
.get()
|
||||||
.then((userCollection) =>
|
.then((userCollection) =>
|
||||||
userCollection.data()?[_options.groupChatsCollectionName])
|
userCollection.data()?[_options.groupChatsCollectionName])
|
||||||
.then((groupChatLabels) => groupChatLabels?.cast<String>());
|
.then((groupChatLabels) => groupChatLabels?.cast<String>())
|
||||||
|
.then((groupChatIds) {
|
||||||
|
var startIndex = (pageNumber - 1) * pageSize;
|
||||||
|
var endIndex = startIndex + pageSize;
|
||||||
|
|
||||||
var chatsStream =
|
if (groupChatIds != null) {
|
||||||
_getSpecificChatsStream([...userChatIds, ...userGroupChatIds]);
|
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) {
|
chatsSubscription = chatsStream.listen((event) {
|
||||||
controller.add(event);
|
controller.add(event);
|
||||||
|
@ -270,7 +304,7 @@ class FirebaseChatService implements ChatService {
|
||||||
var collection = await _db
|
var collection = await _db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(currentUser?.id)
|
.doc(currentUser?.id)
|
||||||
.collection('chats')
|
.collection(_options.userChatsCollectionName)
|
||||||
.where('users', arrayContains: user.id)
|
.where('users', arrayContains: user.id)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
|
@ -288,7 +322,7 @@ class FirebaseChatService implements ChatService {
|
||||||
var chatCollection = await _db
|
var chatCollection = await _db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(currentUser?.id)
|
.doc(currentUser?.id)
|
||||||
.collection('chats')
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(chatId)
|
.doc(chatId)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
|
@ -333,7 +367,7 @@ class FirebaseChatService implements ChatService {
|
||||||
@override
|
@override
|
||||||
Future<void> deleteChat(ChatModel chat) async {
|
Future<void> deleteChat(ChatModel chat) async {
|
||||||
var chatCollection = await _db
|
var chatCollection = await _db
|
||||||
.collection(_options.chatsCollectionName)
|
.collection(_options.chatsMetaDataCollectionName)
|
||||||
.doc(chat.id)
|
.doc(chat.id)
|
||||||
.withConverter(
|
.withConverter(
|
||||||
fromFirestore: (snapshot, _) =>
|
fromFirestore: (snapshot, _) =>
|
||||||
|
@ -349,7 +383,7 @@ class FirebaseChatService implements ChatService {
|
||||||
_db
|
_db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection('chats')
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(chat.id)
|
.doc(chat.id)
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
|
@ -387,7 +421,7 @@ class FirebaseChatService implements ChatService {
|
||||||
];
|
];
|
||||||
|
|
||||||
var reference = await _db
|
var reference = await _db
|
||||||
.collection(_options.chatsCollectionName)
|
.collection(_options.chatsMetaDataCollectionName)
|
||||||
.withConverter(
|
.withConverter(
|
||||||
fromFirestore: (snapshot, _) =>
|
fromFirestore: (snapshot, _) =>
|
||||||
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
|
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
|
||||||
|
@ -406,12 +440,13 @@ class FirebaseChatService implements ChatService {
|
||||||
await _db
|
await _db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection('chats')
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(reference.id)
|
.doc(reference.id)
|
||||||
.set({'users': userIds});
|
.set({'users': userIds});
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
@ -448,6 +483,7 @@ class FirebaseChatService implements ChatService {
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
||||||
}
|
}
|
||||||
|
@ -467,7 +503,7 @@ class FirebaseChatService implements ChatService {
|
||||||
var userSnapshot = _db
|
var userSnapshot = _db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(currentUser?.id)
|
.doc(currentUser?.id)
|
||||||
.collection('chats')
|
.collection(_options.userChatsCollectionName)
|
||||||
.snapshots();
|
.snapshots();
|
||||||
|
|
||||||
unreadChatSubscription = userSnapshot.listen((event) {
|
unreadChatSubscription = userSnapshot.listen((event) {
|
||||||
|
@ -498,7 +534,7 @@ class FirebaseChatService implements ChatService {
|
||||||
await _db
|
await _db
|
||||||
.collection(_options.usersCollectionName)
|
.collection(_options.usersCollectionName)
|
||||||
.doc(currentUser!.id!)
|
.doc(currentUser!.id!)
|
||||||
.collection('chats')
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(chat.id)
|
.doc(chat.id)
|
||||||
.set({'amount_unread_messages': 0}, SetOptions(merge: true));
|
.set({'amount_unread_messages': 0}, SetOptions(merge: true));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import 'package:flutter_community_chat_firebase/dto/firebase_message_document.da
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class FirebaseMessageService implements MessageService {
|
class FirebaseMessageService with ChangeNotifier implements MessageService {
|
||||||
late final FirebaseFirestore _db;
|
late final FirebaseFirestore _db;
|
||||||
late final FirebaseStorage _storage;
|
late final FirebaseStorage _storage;
|
||||||
late final ChatUserService _userService;
|
late final ChatUserService _userService;
|
||||||
|
@ -21,6 +21,11 @@ class FirebaseMessageService implements MessageService {
|
||||||
|
|
||||||
StreamController<List<ChatMessageModel>>? _controller;
|
StreamController<List<ChatMessageModel>>? _controller;
|
||||||
StreamSubscription<QuerySnapshot>? _subscription;
|
StreamSubscription<QuerySnapshot>? _subscription;
|
||||||
|
DocumentSnapshot<Object>? lastMessage;
|
||||||
|
List<ChatMessageModel> _cumulativeMessages = [];
|
||||||
|
ChatModel? lastChat;
|
||||||
|
int? chatPageSize;
|
||||||
|
DateTime timestampToFilter = DateTime.now();
|
||||||
|
|
||||||
FirebaseMessageService({
|
FirebaseMessageService({
|
||||||
required ChatUserService userService,
|
required ChatUserService userService,
|
||||||
|
@ -54,13 +59,28 @@ class FirebaseMessageService implements MessageService {
|
||||||
)
|
)
|
||||||
.doc(chat.id);
|
.doc(chat.id);
|
||||||
|
|
||||||
await chatReference
|
var newMessage = await chatReference
|
||||||
.collection(
|
.collection(
|
||||||
_options.messagesCollectionName,
|
_options.messagesCollectionName,
|
||||||
)
|
)
|
||||||
.add(message);
|
.add(message);
|
||||||
|
|
||||||
await chatReference.update({
|
if (_cumulativeMessages.length == 1) {
|
||||||
|
lastMessage = await chatReference
|
||||||
|
.collection(
|
||||||
|
_options.messagesCollectionName,
|
||||||
|
)
|
||||||
|
.doc(newMessage.id)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadataReference = _db
|
||||||
|
.collection(
|
||||||
|
_options.chatsMetaDataCollectionName,
|
||||||
|
)
|
||||||
|
.doc(chat.id);
|
||||||
|
|
||||||
|
await metadataReference.update({
|
||||||
'last_used': DateTime.now(),
|
'last_used': DateTime.now(),
|
||||||
'last_message': message,
|
'last_message': message,
|
||||||
});
|
});
|
||||||
|
@ -76,7 +96,7 @@ class FirebaseMessageService implements MessageService {
|
||||||
// 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
|
||||||
var fetchedChat = await chatReference.get();
|
var fetchedChat = await metadataReference.get();
|
||||||
var chatUsers = fetchedChat.data()?['users'] as List<dynamic>;
|
var chatUsers = fetchedChat.data()?['users'] as List<dynamic>;
|
||||||
// for all users except the message sender update the unread counter
|
// for all users except the message sender update the unread counter
|
||||||
for (var userId in chatUsers) {
|
for (var userId in chatUsers) {
|
||||||
|
@ -86,7 +106,7 @@ class FirebaseMessageService implements MessageService {
|
||||||
_options.usersCollectionName,
|
_options.usersCollectionName,
|
||||||
)
|
)
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection('chats')
|
.collection(_options.userChatsCollectionName)
|
||||||
.doc(chat.id);
|
.doc(chat.id);
|
||||||
// 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
|
||||||
|
@ -110,13 +130,14 @@ class FirebaseMessageService implements MessageService {
|
||||||
Future<void> sendTextMessage({
|
Future<void> sendTextMessage({
|
||||||
required String text,
|
required String text,
|
||||||
required ChatModel chat,
|
required ChatModel chat,
|
||||||
}) =>
|
}) {
|
||||||
_sendMessage(
|
return _sendMessage(
|
||||||
chat,
|
chat,
|
||||||
{
|
{
|
||||||
'text': text,
|
'text': text,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> sendImageMessage({
|
Future<void> sendImageMessage({
|
||||||
|
@ -144,47 +165,81 @@ class FirebaseMessageService implements MessageService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Query<FirebaseMessageDocument> _getMessagesQuery(ChatModel chat) => _db
|
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)
|
.collection(_options.chatsCollectionName)
|
||||||
.doc(chat.id)
|
.doc(chat.id)
|
||||||
.collection(_options.messagesCollectionName)
|
.collection(_options.messagesCollectionName)
|
||||||
.orderBy('timestamp', descending: false)
|
.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>(
|
.withConverter<FirebaseMessageDocument>(
|
||||||
fromFirestore: (snapshot, _) =>
|
fromFirestore: (snapshot, _) =>
|
||||||
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
|
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
|
||||||
toFirestore: (user, _) => user.toJson(),
|
toFirestore: (user, _) => user.toJson(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) {
|
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) {
|
||||||
_controller = StreamController<List<ChatMessageModel>>(
|
_controller = StreamController<List<ChatMessageModel>>(
|
||||||
onListen: () {
|
onListen: () {
|
||||||
if (chat.id != null) {
|
var messagesCollection = _db
|
||||||
_subscription = _startListeningForMessages(chat);
|
.collection(_options.chatsCollectionName)
|
||||||
}
|
.doc(chat.id)
|
||||||
},
|
.collection(_options.messagesCollectionName)
|
||||||
onCancel: () {
|
.withConverter<FirebaseMessageDocument>(
|
||||||
_subscription?.cancel();
|
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
|
||||||
_subscription = null;
|
snapshot.data()!, snapshot.id),
|
||||||
debugPrint('Canceling messages stream');
|
toFirestore: (user, _) => user.toJson(),
|
||||||
},
|
);
|
||||||
|
var query = messagesCollection
|
||||||
|
.where(
|
||||||
|
'timestamp',
|
||||||
|
isGreaterThan: timestampToFilter,
|
||||||
|
)
|
||||||
|
.withConverter<FirebaseMessageDocument>(
|
||||||
|
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
|
||||||
|
snapshot.data()!, snapshot.id),
|
||||||
|
toFirestore: (user, _) => user.toJson(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return _controller!.stream;
|
var stream = query.snapshots();
|
||||||
}
|
// Subscribe to the stream and process the updates
|
||||||
|
_subscription = stream.listen((snapshot) async {
|
||||||
StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) {
|
|
||||||
debugPrint('Start listening for messages in chat ${chat.id}');
|
|
||||||
|
|
||||||
var snapshots = _getMessagesQuery(chat).snapshots();
|
|
||||||
|
|
||||||
return snapshots.listen(
|
|
||||||
(snapshot) async {
|
|
||||||
var messages = <ChatMessageModel>[];
|
var messages = <ChatMessageModel>[];
|
||||||
|
|
||||||
for (var messageDoc in snapshot.docs) {
|
for (var messageDoc in snapshot.docs) {
|
||||||
var messageData = messageDoc.data();
|
var messageData = messageDoc.data();
|
||||||
|
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
(messageData.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);
|
var sender = await _userService.getUser(messageData.sender);
|
||||||
|
|
||||||
if (sender != null) {
|
if (sender != null) {
|
||||||
|
@ -207,9 +262,171 @@ class FirebaseMessageService implements MessageService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the filtered messages to the controller
|
||||||
|
_controller?.add(messages);
|
||||||
|
_cumulativeMessages = [
|
||||||
|
..._cumulativeMessages,
|
||||||
|
...messages,
|
||||||
|
];
|
||||||
|
|
||||||
|
// remove all double elements
|
||||||
|
List<ChatMessageModel> uniqueObjects =
|
||||||
|
_cumulativeMessages.toSet().toList();
|
||||||
|
_cumulativeMessages = uniqueObjects;
|
||||||
|
_cumulativeMessages
|
||||||
|
.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
notifyListeners();
|
||||||
|
timestampToFilter = DateTime.now();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_subscription = null;
|
||||||
|
debugPrint('Canceling messages stream');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return _controller!.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) {
|
||||||
|
debugPrint('Start listening for messages in chat ${chat.id}');
|
||||||
|
var snapshots = _getMessagesQuery(chat).snapshots();
|
||||||
|
return snapshots.listen(
|
||||||
|
(snapshot) async {
|
||||||
|
List<ChatMessageModel> messages =
|
||||||
|
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);
|
_controller?.add(messages);
|
||||||
|
notifyListeners();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> fetchMoreMessage(int pageSize, ChatModel chat) async {
|
||||||
|
if (lastChat == null) {
|
||||||
|
lastChat = chat;
|
||||||
|
} else if (lastChat?.id != chat.id) {
|
||||||
|
_cumulativeMessages = [];
|
||||||
|
lastChat = chat;
|
||||||
|
lastMessage = null;
|
||||||
|
}
|
||||||
|
// get the x amount of last messages from the oldest message that is in cumulative messages and add that to the list
|
||||||
|
List<ChatMessageModel> messages = [];
|
||||||
|
QuerySnapshot<FirebaseMessageDocument>? messagesQuerySnapshot;
|
||||||
|
var query = _db
|
||||||
|
.collection(_options.chatsCollectionName)
|
||||||
|
.doc(chat.id)
|
||||||
|
.collection(_options.messagesCollectionName)
|
||||||
|
.orderBy('timestamp', descending: true)
|
||||||
|
.limit(pageSize);
|
||||||
|
if (lastMessage == null) {
|
||||||
|
messagesQuerySnapshot = await query
|
||||||
|
.withConverter<FirebaseMessageDocument>(
|
||||||
|
fromFirestore: (snapshot, _) =>
|
||||||
|
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
|
||||||
|
toFirestore: (user, _) => user.toJson(),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (messagesQuerySnapshot.docs.isNotEmpty) {
|
||||||
|
lastMessage = messagesQuerySnapshot.docs.last;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messagesQuerySnapshot = await query
|
||||||
|
.startAfterDocument(lastMessage!)
|
||||||
|
.withConverter<FirebaseMessageDocument>(
|
||||||
|
fromFirestore: (snapshot, _) =>
|
||||||
|
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
|
||||||
|
toFirestore: (user, _) => user.toJson(),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (messagesQuerySnapshot.docs.isNotEmpty) {
|
||||||
|
lastMessage = messagesQuerySnapshot.docs.last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FirebaseMessageDocument> messageDocuments = messagesQuerySnapshot.docs
|
||||||
|
.map((QueryDocumentSnapshot<FirebaseMessageDocument> doc) => doc.data())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (var message in messageDocuments) {
|
||||||
|
var sender = await _userService.getUser(message.sender);
|
||||||
|
if (sender != null) {
|
||||||
|
var timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
(message.timestamp).millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
|
||||||
|
messages.add(
|
||||||
|
message.imageUrl != null
|
||||||
|
? ChatImageMessageModel(
|
||||||
|
sender: sender,
|
||||||
|
imageUrl: message.imageUrl!,
|
||||||
|
timestamp: timestamp,
|
||||||
|
)
|
||||||
|
: ChatTextMessageModel(
|
||||||
|
sender: sender,
|
||||||
|
text: message.text!,
|
||||||
|
timestamp: timestamp,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_cumulativeMessages = [
|
||||||
|
...messages,
|
||||||
|
..._cumulativeMessages,
|
||||||
|
];
|
||||||
|
_cumulativeMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ChatMessageModel> getMessages() {
|
||||||
|
return _cumulativeMessages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
name: flutter_community_chat_firebase
|
name: flutter_community_chat_firebase
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 0.6.0
|
version: 1.0.0
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
@ -23,7 +23,7 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat
|
url: https://github.com/Iconica-Development/flutter_community_chat
|
||||||
path: packages/flutter_community_chat_interface
|
path: packages/flutter_community_chat_interface
|
||||||
ref: 0.6.0
|
ref: 1.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
||||||
|
|
||||||
abstract class ChatService {
|
abstract class ChatService {
|
||||||
Stream<List<ChatModel>> getChatsStream();
|
Stream<List<ChatModel>> getChatsStream(int pageSize);
|
||||||
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);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
||||||
|
|
||||||
abstract class MessageService {
|
abstract class MessageService with ChangeNotifier {
|
||||||
Future<void> sendTextMessage({
|
Future<void> sendTextMessage({
|
||||||
required ChatModel chat,
|
required ChatModel chat,
|
||||||
required String text,
|
required String text,
|
||||||
|
@ -15,4 +16,8 @@ abstract class MessageService {
|
||||||
Stream<List<ChatMessageModel>> getMessagesStream(
|
Stream<List<ChatMessageModel>> getMessagesStream(
|
||||||
ChatModel chat,
|
ChatModel chat,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<void> fetchMoreMessage(int pageSize, ChatModel chat);
|
||||||
|
|
||||||
|
List<ChatMessageModel> getMessages();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
name: flutter_community_chat_interface
|
name: flutter_community_chat_interface
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 0.6.0
|
version: 1.0.0
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
// This is a generated file; do not edit or check into version control.
|
||||||
|
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
|
||||||
|
COCOAPODS_PARALLEL_CODE_SIGN=true
|
||||||
|
FLUTTER_TARGET=lib/main.dart
|
||||||
|
FLUTTER_BUILD_DIR=build
|
||||||
|
FLUTTER_BUILD_NAME=0.6.0
|
||||||
|
FLUTTER_BUILD_NUMBER=0.6.0
|
||||||
|
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||||
|
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
|
||||||
|
DART_OBFUSCATION=false
|
||||||
|
TRACK_WIDGET_CREATION=true
|
||||||
|
TREE_SHAKE_ICONS=false
|
||||||
|
PACKAGE_CONFIG=.dart_tool/package_config.json
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# 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_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_community_chat/packages/flutter_community_chat_view"
|
||||||
|
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||||
|
export "FLUTTER_TARGET=lib/main.dart"
|
||||||
|
export "FLUTTER_BUILD_DIR=build"
|
||||||
|
export "FLUTTER_BUILD_NAME=0.6.0"
|
||||||
|
export "FLUTTER_BUILD_NUMBER=0.6.0"
|
||||||
|
export "DART_OBFUSCATION=false"
|
||||||
|
export "TRACK_WIDGET_CREATION=true"
|
||||||
|
export "TREE_SHAKE_ICONS=false"
|
||||||
|
export "PACKAGE_CONFIG=.dart_tool/package_config.json"
|
41
packages/flutter_community_chat_view/ios/Podfile
Normal file
41
packages/flutter_community_chat_view/ios/Podfile
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
|
# platform :ios, '11.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GeneratedPluginRegistrant_h
|
||||||
|
#define GeneratedPluginRegistrant_h
|
||||||
|
|
||||||
|
#import <Flutter/Flutter.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface GeneratedPluginRegistrant : NSObject
|
||||||
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
#endif /* GeneratedPluginRegistrant_h */
|
|
@ -0,0 +1,63 @@
|
||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
|
||||||
|
#if __has_include(<cloud_firestore/FLTFirebaseFirestorePlugin.h>)
|
||||||
|
#import <cloud_firestore/FLTFirebaseFirestorePlugin.h>
|
||||||
|
#else
|
||||||
|
@import cloud_firestore;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<firebase_auth/FLTFirebaseAuthPlugin.h>)
|
||||||
|
#import <firebase_auth/FLTFirebaseAuthPlugin.h>
|
||||||
|
#else
|
||||||
|
@import firebase_auth;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<firebase_core/FLTFirebaseCorePlugin.h>)
|
||||||
|
#import <firebase_core/FLTFirebaseCorePlugin.h>
|
||||||
|
#else
|
||||||
|
@import firebase_core;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<firebase_storage/FLTFirebaseStoragePlugin.h>)
|
||||||
|
#import <firebase_storage/FLTFirebaseStoragePlugin.h>
|
||||||
|
#else
|
||||||
|
@import firebase_storage;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<image_picker_ios/FLTImagePickerPlugin.h>)
|
||||||
|
#import <image_picker_ios/FLTImagePickerPlugin.h>
|
||||||
|
#else
|
||||||
|
@import image_picker_ios;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<path_provider_foundation/PathProviderPlugin.h>)
|
||||||
|
#import <path_provider_foundation/PathProviderPlugin.h>
|
||||||
|
#else
|
||||||
|
@import path_provider_foundation;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<sqflite/SqflitePlugin.h>)
|
||||||
|
#import <sqflite/SqflitePlugin.h>
|
||||||
|
#else
|
||||||
|
@import sqflite;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
|
[FLTFirebaseFirestorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseFirestorePlugin"]];
|
||||||
|
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
|
||||||
|
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
||||||
|
[FLTFirebaseStoragePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseStoragePlugin"]];
|
||||||
|
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||||
|
[PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]];
|
||||||
|
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -34,22 +34,18 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var isNewDate = widget.previousMessage != null &&
|
var isNewDate = widget.previousMessage != null &&
|
||||||
widget.message.timestamp.day != widget.previousMessage!.timestamp.day;
|
widget.message.timestamp.day != widget.previousMessage?.timestamp.day;
|
||||||
|
var isSameSender = widget.previousMessage == null ||
|
||||||
|
widget.previousMessage?.sender.id != widget.message.sender.id;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: isNewDate ||
|
top: isNewDate || isSameSender ? 25.0 : 0,
|
||||||
widget.previousMessage == null ||
|
|
||||||
widget.previousMessage?.sender.id != widget.message.sender.id
|
|
||||||
? 25.0
|
|
||||||
: 0,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (isNewDate ||
|
if (isNewDate || isSameSender) ...[
|
||||||
widget.previousMessage == null ||
|
|
||||||
widget.previousMessage?.sender.id !=
|
|
||||||
widget.message.sender.id) ...[
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 10.0),
|
padding: const EdgeInsets.only(left: 10.0),
|
||||||
child: widget.message.sender.imageUrl != null &&
|
child: widget.message.sender.imageUrl != null &&
|
||||||
|
@ -75,19 +71,20 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (isNewDate ||
|
if (isNewDate || isSameSender)
|
||||||
widget.previousMessage == null ||
|
|
||||||
widget.previousMessage?.sender.id !=
|
|
||||||
widget.message.sender.id)
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.message.sender.fullName?.toUpperCase() ??
|
widget.message.sender.fullName?.toUpperCase() ??
|
||||||
widget.translations.anonymousUser,
|
widget.translations.anonymousUser,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelMedium
|
||||||
|
?.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -112,7 +109,13 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: (widget.message as ChatTextMessageModel)
|
text: (widget.message as ChatTextMessageModel)
|
||||||
.text,
|
.text,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelMedium
|
||||||
|
?.color,
|
||||||
|
),
|
||||||
children: <TextSpan>[
|
children: <TextSpan>[
|
||||||
if (widget.showTime)
|
if (widget.showTime)
|
||||||
TextSpan(
|
TextSpan(
|
||||||
|
|
|
@ -24,9 +24,11 @@ class ChatImage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
child: CachedNetworkImage(
|
child: image != ''
|
||||||
|
? CachedNetworkImage(
|
||||||
imageUrl: image,
|
imageUrl: image,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ Widget _createImagePickerContainer(
|
||||||
) =>
|
) =>
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
color: Colors.black,
|
color: Colors.white,
|
||||||
child: ImagePicker(
|
child: ImagePicker(
|
||||||
customButton: ElevatedButton(
|
customButton: ElevatedButton(
|
||||||
onPressed: onClose,
|
onPressed: onClose,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/flutter_community_chat_view.dart';
|
||||||
import 'package:flutter_community_chat_view/src/components/chat_bottom.dart';
|
import 'package:flutter_community_chat_view/src/components/chat_bottom.dart';
|
||||||
import 'package:flutter_community_chat_view/src/components/chat_detail_row.dart';
|
import 'package:flutter_community_chat_view/src/components/chat_detail_row.dart';
|
||||||
|
@ -20,6 +21,7 @@ class ChatDetailScreen extends StatefulWidget {
|
||||||
required this.service,
|
required this.service,
|
||||||
required this.chatUserService,
|
required this.chatUserService,
|
||||||
required this.messageService,
|
required this.messageService,
|
||||||
|
required this.pageSize,
|
||||||
this.translations = const ChatTranslations(),
|
this.translations = const ChatTranslations(),
|
||||||
this.chat,
|
this.chat,
|
||||||
this.onPressChatTitle,
|
this.onPressChatTitle,
|
||||||
|
@ -47,6 +49,7 @@ class ChatDetailScreen extends StatefulWidget {
|
||||||
final ChatService service;
|
final ChatService service;
|
||||||
final ChatUserService chatUserService;
|
final ChatUserService chatUserService;
|
||||||
final MessageService messageService;
|
final MessageService messageService;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatDetailScreen> createState() => _ChatDetailScreenState();
|
State<ChatDetailScreen> createState() => _ChatDetailScreenState();
|
||||||
|
@ -54,32 +57,29 @@ class ChatDetailScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
// stream listener that needs to be disposed later
|
// stream listener that needs to be disposed later
|
||||||
StreamSubscription<List<ChatMessageModel>>? _chatMessagesSubscription;
|
|
||||||
Stream<List<ChatMessageModel>>? _chatMessages;
|
|
||||||
ChatModel? chat;
|
|
||||||
ChatUserModel? currentUser;
|
ChatUserModel? currentUser;
|
||||||
|
ScrollController controller = ScrollController();
|
||||||
|
bool showIndicator = false;
|
||||||
|
late MessageService messageSubscription;
|
||||||
|
Stream<List<ChatMessageModel>>? stream;
|
||||||
|
ChatMessageModel? previousMessage;
|
||||||
|
List<Widget> detailRows = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// create a broadcast stream from the chat messages
|
messageSubscription = widget.messageService;
|
||||||
|
messageSubscription.addListener(onListen);
|
||||||
if (widget.chat != null) {
|
if (widget.chat != null) {
|
||||||
_chatMessages = widget.messageService
|
stream = widget.messageService.getMessagesStream(widget.chat!);
|
||||||
.getMessagesStream(widget.chat!)
|
stream?.listen((event) {});
|
||||||
.asBroadcastStream();
|
|
||||||
}
|
|
||||||
_chatMessagesSubscription = _chatMessages?.listen((event) {
|
|
||||||
// check if the last message is from the current user
|
|
||||||
// if so, set the chat to read
|
|
||||||
Future.delayed(Duration.zero, () async {
|
Future.delayed(Duration.zero, () async {
|
||||||
currentUser = await widget.chatUserService.getCurrentUser();
|
if (detailRows.isEmpty) {
|
||||||
});
|
await widget.messageService
|
||||||
if (event.isNotEmpty &&
|
.fetchMoreMessage(widget.pageSize, widget.chat!);
|
||||||
event.last.sender.id != currentUser?.id &&
|
|
||||||
widget.chat != null) {
|
|
||||||
widget.onReadChat(widget.chat!);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (widget.chat != null) {
|
if (widget.chat != null) {
|
||||||
widget.onReadChat(widget.chat!);
|
widget.onReadChat(widget.chat!);
|
||||||
|
@ -87,9 +87,34 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onListen() {
|
||||||
|
var chatMessages = [];
|
||||||
|
chatMessages = widget.messageService.getMessages();
|
||||||
|
detailRows = [];
|
||||||
|
previousMessage = null;
|
||||||
|
for (var message in chatMessages) {
|
||||||
|
detailRows.add(
|
||||||
|
ChatDetailRow(
|
||||||
|
showTime: true,
|
||||||
|
message: message,
|
||||||
|
translations: widget.translations,
|
||||||
|
userAvatarBuilder: widget.options.userAvatarBuilder,
|
||||||
|
previousMessage: previousMessage,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
previousMessage = message;
|
||||||
|
}
|
||||||
|
detailRows = detailRows.reversed.toList();
|
||||||
|
|
||||||
|
widget.onReadChat(widget.chat!);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_chatMessagesSubscription?.cancel();
|
messageSubscription.removeListener(onListen);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +146,6 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
future: widget.service.getChatById(widget.chat?.id ?? ''),
|
future: widget.service.getChatById(widget.chat?.id ?? ''),
|
||||||
builder: (context, AsyncSnapshot<ChatModel> snapshot) {
|
builder: (context, AsyncSnapshot<ChatModel> snapshot) {
|
||||||
var chatModel = snapshot.data;
|
var chatModel = snapshot.data;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
|
@ -166,33 +190,48 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder<List<ChatMessageModel>>(
|
child: Listener(
|
||||||
stream: _chatMessages,
|
onPointerMove: (event) async {
|
||||||
builder: (context, snapshot) {
|
var isTop = controller.position.pixels ==
|
||||||
var messages = snapshot.data ?? chatModel?.messages ?? [];
|
controller.position.maxScrollExtent;
|
||||||
ChatMessageModel? previousMessage;
|
|
||||||
|
|
||||||
var messageWidgets = <Widget>[];
|
if (showIndicator == false &&
|
||||||
|
!isTop &&
|
||||||
for (var message in messages) {
|
controller.position.userScrollDirection ==
|
||||||
messageWidgets.add(
|
ScrollDirection.reverse) {
|
||||||
ChatDetailRow(
|
setState(() {
|
||||||
previousMessage: previousMessage,
|
showIndicator = true;
|
||||||
showTime: widget.showTime,
|
});
|
||||||
translations: widget.translations,
|
await widget.messageService
|
||||||
message: message,
|
.fetchMoreMessage(widget.pageSize, widget.chat!);
|
||||||
userAvatarBuilder: widget.options.userAvatarBuilder,
|
Future.delayed(const Duration(seconds: 2), () {
|
||||||
),
|
if (mounted) {
|
||||||
);
|
setState(() {
|
||||||
previousMessage = message;
|
showIndicator = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return ListView(
|
}
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
controller: controller,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
padding: const EdgeInsets.only(top: 24.0),
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
children: messageWidgets.reversed.toList(),
|
children: [
|
||||||
);
|
...detailRows,
|
||||||
},
|
if (showIndicator) ...[
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (chatModel != null)
|
if (chatModel != null)
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/flutter_community_chat_view.dart';
|
||||||
import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
|
import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
|
||||||
|
|
||||||
|
@ -15,6 +18,7 @@ class ChatScreen extends StatefulWidget {
|
||||||
required this.onPressChat,
|
required this.onPressChat,
|
||||||
required this.onDeleteChat,
|
required this.onDeleteChat,
|
||||||
required this.service,
|
required this.service,
|
||||||
|
required this.pageSize,
|
||||||
this.onNoChats,
|
this.onNoChats,
|
||||||
this.deleteChatDialog,
|
this.deleteChatDialog,
|
||||||
this.translations = const ChatTranslations(),
|
this.translations = const ChatTranslations(),
|
||||||
|
@ -25,10 +29,11 @@ class ChatScreen extends StatefulWidget {
|
||||||
final ChatOptions options;
|
final ChatOptions options;
|
||||||
final ChatTranslations translations;
|
final ChatTranslations translations;
|
||||||
final ChatService service;
|
final ChatService service;
|
||||||
final VoidCallback? onPressStartChat;
|
final Function()? onPressStartChat;
|
||||||
final VoidCallback? onNoChats;
|
final Function()? onNoChats;
|
||||||
final void Function(ChatModel chat) onDeleteChat;
|
final void Function(ChatModel chat) onDeleteChat;
|
||||||
final void Function(ChatModel chat) onPressChat;
|
final void Function(ChatModel chat) onPressChat;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
/// Disable the swipe to dismiss feature for chats that are not deletable
|
/// Disable the swipe to dismiss feature for chats that are not deletable
|
||||||
final bool disableDismissForPermanentChats;
|
final bool disableDismissForPermanentChats;
|
||||||
|
@ -42,6 +47,28 @@ class ChatScreen extends StatefulWidget {
|
||||||
class _ChatScreenState extends State<ChatScreen> {
|
class _ChatScreenState extends State<ChatScreen> {
|
||||||
final DateFormatter _dateFormatter = DateFormatter();
|
final DateFormatter _dateFormatter = DateFormatter();
|
||||||
bool _hasCalledOnNoChats = false;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -72,19 +99,45 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
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(
|
child: ListView(
|
||||||
|
controller: controller,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.only(top: 15.0),
|
padding: const EdgeInsets.only(top: 15.0),
|
||||||
children: [
|
children: [
|
||||||
StreamBuilder<List<ChatModel>>(
|
StreamBuilder<List<ChatModel>>(
|
||||||
stream: widget.service.getChatsStream(),
|
stream: chats,
|
||||||
builder: (BuildContext context, snapshot) {
|
builder: (BuildContext context, snapshot) {
|
||||||
// if the stream is done, empty and noChats is set we should call that
|
// if the stream is done, empty and noChats is set we should call that
|
||||||
if (snapshot.connectionState == ConnectionState.done &&
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
(snapshot.data?.isEmpty ?? true)) {
|
(snapshot.data?.isEmpty ?? true)) {
|
||||||
if (widget.onNoChats != null && !_hasCalledOnNoChats) {
|
if (widget.onNoChats != null && !_hasCalledOnNoChats) {
|
||||||
_hasCalledOnNoChats = true; // Set the flag to true
|
_hasCalledOnNoChats = true; // Set the flag to true
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance
|
||||||
widget.onNoChats!.call();
|
.addPostFrameCallback((_) async {
|
||||||
|
await widget.onNoChats!.call();
|
||||||
|
getChats();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -93,20 +146,23 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
for (ChatModel chat in snapshot.data ?? []) ...[
|
for (ChatModel chat in (snapshot.data ?? []).where(
|
||||||
|
(chat) => !deletedChats.contains(chat.id),
|
||||||
|
)) ...[
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) => !(widget
|
builder: (context) => !(widget
|
||||||
.disableDismissForPermanentChats &&
|
.disableDismissForPermanentChats &&
|
||||||
!chat.canBeDeleted)
|
!chat.canBeDeleted)
|
||||||
? Dismissible(
|
? Dismissible(
|
||||||
confirmDismiss: (_) =>
|
confirmDismiss: (_) async =>
|
||||||
widget.deleteChatDialog
|
widget.deleteChatDialog
|
||||||
?.call(context, chat) ??
|
?.call(context, chat) ??
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) =>
|
builder: (BuildContext context) =>
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding:
|
||||||
|
const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
@ -118,7 +174,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
.chatCantBeDeleted,
|
.chatCantBeDeleted,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight:
|
||||||
|
FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
@ -133,19 +190,22 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.center,
|
MainAxisAlignment
|
||||||
|
.center,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
translations
|
translations
|
||||||
.deleteChatModalCancel,
|
.deleteChatModalCancel,
|
||||||
style: const TextStyle(
|
style:
|
||||||
|
const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
Navigator.of(context)
|
Navigator.of(
|
||||||
.pop(false),
|
context,
|
||||||
|
).pop(false),
|
||||||
),
|
),
|
||||||
if (chat.canBeDeleted)
|
if (chat.canBeDeleted)
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
@ -168,8 +228,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onDismissed: (_) =>
|
onDismissed: (_) {
|
||||||
widget.onDeleteChat(chat),
|
setState(() {
|
||||||
|
deletedChats.add(chat.id!);
|
||||||
|
});
|
||||||
|
widget.onDeleteChat(chat);
|
||||||
|
},
|
||||||
background: Container(
|
background: Container(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
child: Align(
|
child: Align(
|
||||||
|
@ -200,6 +264,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
if (showIndicator &&
|
||||||
|
snapshot.connectionState !=
|
||||||
|
ConnectionState.done) ...[
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -207,10 +282,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (widget.onPressStartChat != null)
|
if (widget.onPressStartChat != null)
|
||||||
widget.options.newChatButtonBuilder(
|
widget.options.newChatButtonBuilder(
|
||||||
context,
|
context,
|
||||||
widget.onPressStartChat!,
|
() async {
|
||||||
|
await widget.onPressStartChat!.call();
|
||||||
|
getChats();
|
||||||
|
},
|
||||||
translations,
|
translations,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -237,6 +316,8 @@ class ChatListItem extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => widget.onPressChat(chat),
|
onTap: () => widget.onPressChat(chat),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
child: widget.options.chatRowContainerBuilder(
|
child: widget.options.chatRowContainerBuilder(
|
||||||
(chat is PersonalChatModel)
|
(chat is PersonalChatModel)
|
||||||
? ChatRow(
|
? ChatRow(
|
||||||
|
@ -280,6 +361,7 @@ class ChatListItem extends StatelessWidget {
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||||
title: user.fullName ?? widget.translations.anonymousUser,
|
title: user.fullName ?? widget.translations.anonymousUser,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => widget.onPressCreateChat(user),
|
onTap: () async {
|
||||||
|
await widget.onPressCreateChat(user);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
name: flutter_community_chat_view
|
name: flutter_community_chat_view
|
||||||
description: A standard flutter package.
|
description: A standard flutter package.
|
||||||
version: 0.6.0
|
version: 1.0.0
|
||||||
|
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_community_chat
|
url: https://github.com/Iconica-Development/flutter_community_chat
|
||||||
path: packages/flutter_community_chat_interface
|
path: packages/flutter_community_chat_interface
|
||||||
ref: 0.6.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:
|
||||||
|
|
Loading…
Reference in a new issue