diff --git a/packages/flutter_community_chat/lib/src/flutter_community_chat_userstory.dart b/packages/flutter_community_chat/lib/src/flutter_community_chat_userstory.dart index 9ed3965..6652ad5 100644 --- a/packages/flutter_community_chat/lib/src/flutter_community_chat_userstory.dart +++ b/packages/flutter_community_chat/lib/src/flutter_community_chat_userstory.dart @@ -17,19 +17,21 @@ List getCommunityChatStoryRoutes( path: CommunityChatUserStoryRoutes.chatScreen, pageBuilder: (context, state) { var chatScreen = ChatScreen( + pageSize: configuration.pageSize, service: configuration.service, options: configuration.chatOptionsBuilder(context), - onNoChats: () => - context.push(CommunityChatUserStoryRoutes.newChatScreen), - onPressStartChat: () => - configuration.onPressStartChat?.call() ?? - context.push(CommunityChatUserStoryRoutes.newChatScreen), + onNoChats: () async => + await context.push(CommunityChatUserStoryRoutes.newChatScreen), + onPressStartChat: () async => + await configuration.onPressStartChat?.call() ?? + await context.push(CommunityChatUserStoryRoutes.newChatScreen), onPressChat: (chat) => configuration.onPressChat?.call(context, chat) ?? context.push( CommunityChatUserStoryRoutes.chatDetailViewPath(chat.id!)), onDeleteChat: (chat) => - configuration.onDeleteChat?.call(context, chat), + configuration.onDeleteChat?.call(context, chat) ?? + configuration.service.deleteChat(chat), deleteChatDialog: configuration.deleteChatDialog, translations: configuration.translations, ); @@ -52,6 +54,7 @@ List getCommunityChatStoryRoutes( var chatId = state.pathParameters['id']; var chat = PersonalChatModel(user: ChatUserModel(), id: chatId); var chatDetailScreen = ChatDetailScreen( + pageSize: configuration.messagePageSize, options: configuration.chatOptionsBuilder(context), translations: configuration.translations, chatUserService: configuration.userService, @@ -110,8 +113,9 @@ List getCommunityChatStoryRoutes( ); } if (context.mounted) { - context.push(CommunityChatUserStoryRoutes.chatDetailViewPath( - chat.id ?? '')); + await context.push( + CommunityChatUserStoryRoutes.chatDetailViewPath( + chat.id ?? '')); } }); return buildScreenWithoutTransition( diff --git a/packages/flutter_community_chat/lib/src/models/community_chat_configuration.dart b/packages/flutter_community_chat/lib/src/models/community_chat_configuration.dart index 8285517..6d8e04d 100644 --- a/packages/flutter_community_chat/lib/src/models/community_chat_configuration.dart +++ b/packages/flutter_community_chat/lib/src/models/community_chat_configuration.dart @@ -14,6 +14,7 @@ class CommunityChatUserStoryConfiguration { required this.messageService, required this.service, required this.chatOptionsBuilder, + this.pageSize = 10, this.onPressStartChat, this.onPressChat, this.onDeleteChat, @@ -29,6 +30,7 @@ class CommunityChatUserStoryConfiguration { this.chatPageBuilder, this.onPressChatTitle, this.afterMessageSent, + this.messagePageSize = 20, }); final ChatService service; 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. final bool routeToNewChatIfEmpty; + final int pageSize; + final int messagePageSize; final Future Function(BuildContext, ChatModel)? deleteChatDialog; final Function(BuildContext context, ChatModel chat)? onPressChatTitle; diff --git a/packages/flutter_community_chat/pubspec.yaml b/packages/flutter_community_chat/pubspec.yaml index 30ebdc6..5aa1391 100644 --- a/packages/flutter_community_chat/pubspec.yaml +++ b/packages/flutter_community_chat/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_community_chat description: A new Flutter package project. -version: 0.6.0 +version: 1.0.0 publish_to: none @@ -20,12 +20,12 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_community_chat path: packages/flutter_community_chat_view - ref: 0.6.0 + ref: 1.0.0 flutter_community_chat_interface: git: url: https://github.com/Iconica-Development/flutter_community_chat path: packages/flutter_community_chat_interface - ref: 0.6.0 + ref: 1.0.0 dev_dependencies: flutter_lints: ^2.0.0 diff --git a/packages/flutter_community_chat_firebase/android/local.properties b/packages/flutter_community_chat_firebase/android/local.properties new file mode 100644 index 0000000..32db309 --- /dev/null +++ b/packages/flutter_community_chat_firebase/android/local.properties @@ -0,0 +1,2 @@ +sdk.dir=/Users/mikedoornenbal/Library/Android/sdk +flutter.sdk=/opt/homebrew/Caskroom/flutter/3.10.2/flutter \ No newline at end of file diff --git a/packages/flutter_community_chat_firebase/lib/config/firebase_chat_options.dart b/packages/flutter_community_chat_firebase/lib/config/firebase_chat_options.dart index 894e21a..73c6319 100644 --- a/packages/flutter_community_chat_firebase/lib/config/firebase_chat_options.dart +++ b/packages/flutter_community_chat_firebase/lib/config/firebase_chat_options.dart @@ -11,18 +11,26 @@ class FirebaseChatOptions { this.chatsCollectionName = 'chats', this.messagesCollectionName = 'messages', this.usersCollectionName = 'users', + this.chatsMetaDataCollectionName = 'chat_metadata', + this.userChatsCollectionName = 'chats', }); final String groupChatsCollectionName; final String chatsCollectionName; final String messagesCollectionName; final String usersCollectionName; + final String chatsMetaDataCollectionName; + + ///This is the collection inside the user document. + final String userChatsCollectionName; FirebaseChatOptions copyWith({ String? groupChatsCollectionName, String? chatsCollectionName, String? messagesCollectionName, String? usersCollectionName, + String? chatsMetaDataCollectionName, + String? userChatsCollectionName, }) { return FirebaseChatOptions( groupChatsCollectionName: @@ -31,6 +39,10 @@ class FirebaseChatOptions { messagesCollectionName: messagesCollectionName ?? this.messagesCollectionName, usersCollectionName: usersCollectionName ?? this.usersCollectionName, + chatsMetaDataCollectionName: + chatsMetaDataCollectionName ?? this.chatsMetaDataCollectionName, + userChatsCollectionName: + userChatsCollectionName ?? this.userChatsCollectionName, ); } } diff --git a/packages/flutter_community_chat_firebase/lib/service/firebase_chat_service.dart b/packages/flutter_community_chat_firebase/lib/service/firebase_chat_service.dart index 38a9c15..954e1e6 100644 --- a/packages/flutter_community_chat_firebase/lib/service/firebase_chat_service.dart +++ b/packages/flutter_community_chat_firebase/lib/service/firebase_chat_service.dart @@ -15,6 +15,10 @@ class FirebaseChatService implements ChatService { late FirebaseStorage _storage; late ChatUserService _userService; late FirebaseChatOptions _options; + DocumentSnapshot? lastUserDocument; + String? lastGroupId; + List chatIds = []; + int pageNumber = 1; FirebaseChatService({ required ChatUserService userService, @@ -37,7 +41,7 @@ class FirebaseChatService implements ChatService { var snapshots = _db .collection(_options.usersCollectionName) .doc(userId) - .collection('chats') + .collection(_options.userChatsCollectionName) .doc(chatId) .snapshots(); @@ -52,7 +56,7 @@ class FirebaseChatService implements ChatService { Function(List) onReceivedChats, ) { var snapshots = _db - .collection(_options.chatsCollectionName) + .collection(_options.chatsMetaDataCollectionName) .where( FieldPath.documentId, whereIn: chatIds, @@ -228,19 +232,29 @@ class FirebaseChatService implements ChatService { } @override - Stream> getChatsStream() { + Stream> getChatsStream(int pageSize) { late StreamController> controller; StreamSubscription? chatsSubscription; controller = StreamController( onListen: () async { + QuerySnapshot> userSnapshot; + List userChatIds; var currentUser = await _userService.getCurrentUser(); - var userSnapshot = await _db + var userQuery = _db .collection(_options.usersCollectionName) .doc(currentUser?.id) - .collection('chats') - .get(); - var userChatIds = userSnapshot.docs.map((chat) => chat.id).toList(); + .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) @@ -248,10 +262,27 @@ class FirebaseChatService implements ChatService { .get() .then((userCollection) => userCollection.data()?[_options.groupChatsCollectionName]) - .then((groupChatLabels) => groupChatLabels?.cast()); + .then((groupChatLabels) => groupChatLabels?.cast()) + .then((groupChatIds) { + var startIndex = (pageNumber - 1) * pageSize; + var endIndex = startIndex + pageSize; - var chatsStream = - _getSpecificChatsStream([...userChatIds, ...userGroupChatIds]); + if (startIndex >= groupChatIds.length) { + return []; + } + var groupIds = groupChatIds.sublist( + startIndex, endIndex.clamp(0, groupChatIds.length)); + lastGroupId = groupIds.last; + return groupIds; + }); + + if (userSnapshot.docs.isNotEmpty) { + lastUserDocument = userSnapshot.docs.last; + } + + pageNumber++; + chatIds.addAll([...userChatIds, ...userGroupChatIds]); + var chatsStream = _getSpecificChatsStream(chatIds); chatsSubscription = chatsStream.listen((event) { controller.add(event); @@ -270,7 +301,7 @@ class FirebaseChatService implements ChatService { var collection = await _db .collection(_options.usersCollectionName) .doc(currentUser?.id) - .collection('chats') + .collection(_options.userChatsCollectionName) .where('users', arrayContains: user.id) .get(); @@ -288,7 +319,7 @@ class FirebaseChatService implements ChatService { var chatCollection = await _db .collection(_options.usersCollectionName) .doc(currentUser?.id) - .collection('chats') + .collection(_options.userChatsCollectionName) .doc(chatId) .get(); @@ -333,7 +364,7 @@ class FirebaseChatService implements ChatService { @override Future deleteChat(ChatModel chat) async { var chatCollection = await _db - .collection(_options.chatsCollectionName) + .collection(_options.chatsMetaDataCollectionName) .doc(chat.id) .withConverter( fromFirestore: (snapshot, _) => @@ -349,7 +380,7 @@ class FirebaseChatService implements ChatService { _db .collection(_options.usersCollectionName) .doc(userId) - .collection('chats') + .collection(_options.userChatsCollectionName) .doc(chat.id) .delete(); } @@ -387,7 +418,7 @@ class FirebaseChatService implements ChatService { ]; var reference = await _db - .collection(_options.chatsCollectionName) + .collection(_options.chatsMetaDataCollectionName) .withConverter( fromFirestore: (snapshot, _) => FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id), @@ -406,12 +437,13 @@ class FirebaseChatService implements ChatService { await _db .collection(_options.usersCollectionName) .doc(userId) - .collection('chats') + .collection(_options.userChatsCollectionName) .doc(reference.id) .set({'users': userIds}); } chat.id = reference.id; + chatIds.add(chat.id!); } else if (chat is GroupChatModel) { if (currentUser?.id == null) { return chat; @@ -448,6 +480,7 @@ class FirebaseChatService implements ChatService { } chat.id = reference.id; + chatIds.add(chat.id!); } else { throw Exception('Chat type not supported for firebase'); } @@ -467,7 +500,7 @@ class FirebaseChatService implements ChatService { var userSnapshot = _db .collection(_options.usersCollectionName) .doc(currentUser?.id) - .collection('chats') + .collection(_options.userChatsCollectionName) .snapshots(); unreadChatSubscription = userSnapshot.listen((event) { @@ -498,7 +531,7 @@ class FirebaseChatService implements ChatService { await _db .collection(_options.usersCollectionName) .doc(currentUser!.id!) - .collection('chats') + .collection(_options.userChatsCollectionName) .doc(chat.id) .set({'amount_unread_messages': 0}, SetOptions(merge: true)); } diff --git a/packages/flutter_community_chat_firebase/lib/service/firebase_message_service.dart b/packages/flutter_community_chat_firebase/lib/service/firebase_message_service.dart index 6ca4e18..2c982d2 100644 --- a/packages/flutter_community_chat_firebase/lib/service/firebase_message_service.dart +++ b/packages/flutter_community_chat_firebase/lib/service/firebase_message_service.dart @@ -21,6 +21,10 @@ class FirebaseMessageService implements MessageService { StreamController>? _controller; StreamSubscription? _subscription; + DocumentSnapshot? lastMessage; + List _cumulativeMessages = []; + ChatModel? lastChat; + int? chatPageSize; FirebaseMessageService({ required ChatUserService userService, @@ -60,7 +64,13 @@ class FirebaseMessageService implements MessageService { ) .add(message); - await chatReference.update({ + var metadataReference = _db + .collection( + _options.chatsMetaDataCollectionName, + ) + .doc(chat.id); + + await metadataReference.update({ 'last_used': DateTime.now(), 'last_message': message, }); @@ -76,7 +86,7 @@ class FirebaseMessageService implements MessageService { // update the chat counter for the other users // get all users from the chat // 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; // for all users except the message sender update the unread counter for (var userId in chatUsers) { @@ -86,7 +96,7 @@ class FirebaseMessageService implements MessageService { _options.usersCollectionName, ) .doc(userId) - .collection('chats') + .collection(_options.userChatsCollectionName) .doc(chat.id); // what if the amount_unread_messages field does not exist? // it should be created when the chat is create @@ -110,13 +120,14 @@ class FirebaseMessageService implements MessageService { Future sendTextMessage({ required String text, required ChatModel chat, - }) => - _sendMessage( - chat, - { - 'text': text, - }, - ); + }) { + return _sendMessage( + chat, + { + 'text': text, + }, + ); + } @override Future sendImageMessage({ @@ -144,19 +155,42 @@ class FirebaseMessageService implements MessageService { ); } - Query _getMessagesQuery(ChatModel chat) => _db - .collection(_options.chatsCollectionName) - .doc(chat.id) - .collection(_options.messagesCollectionName) - .orderBy('timestamp', descending: false) - .withConverter( + Query _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( fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id), toFirestore: (user, _) => user.toJson(), ); + } + return query + .startAfterDocument(lastMessage!) + .withConverter( + fromFirestore: (snapshot, _) => + FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id), + toFirestore: (user, _) => user.toJson(), + ); + } @override - Stream> getMessagesStream(ChatModel chat) { + Stream> getMessagesStream( + ChatModel chat, int pageSize) { + chatPageSize = pageSize; _controller = StreamController>( onListen: () { if (chat.id != null) { @@ -175,39 +209,54 @@ class FirebaseMessageService implements MessageService { StreamSubscription _startListeningForMessages(ChatModel chat) { debugPrint('Start listening for messages in chat ${chat.id}'); - var snapshots = _getMessagesQuery(chat).snapshots(); - return snapshots.listen( (snapshot) async { - var messages = []; + List messages = + List.from(_cumulativeMessages); - for (var messageDoc in snapshot.docs) { - var messageData = messageDoc.data(); + if (snapshot.docs.isNotEmpty) { + lastMessage = snapshot.docs.last; - var sender = await _userService.getUser(messageData.sender); + for (var messageDoc in snapshot.docs) { + var messageData = messageDoc.data(); - if (sender != null) { - var timestamp = DateTime.fromMillisecondsSinceEpoch( - (messageData.timestamp).millisecondsSinceEpoch, - ); + // 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); - messages.add( - messageData.imageUrl != null - ? ChatImageMessageModel( - sender: sender, - imageUrl: messageData.imageUrl!, - timestamp: timestamp, - ) - : ChatTextMessageModel( - sender: sender, - text: messageData.text!, - timestamp: timestamp, - ), - ); + 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); }, ); diff --git a/packages/flutter_community_chat_firebase/pubspec.yaml b/packages/flutter_community_chat_firebase/pubspec.yaml index c30d386..c0d976c 100644 --- a/packages/flutter_community_chat_firebase/pubspec.yaml +++ b/packages/flutter_community_chat_firebase/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_community_chat_firebase description: A new Flutter package project. -version: 0.6.0 +version: 1.0.0 publish_to: none environment: @@ -23,7 +23,7 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_community_chat path: packages/flutter_community_chat_interface - ref: 0.6.0 + ref: 1.0.0 dev_dependencies: flutter_lints: ^2.0.0 diff --git a/packages/flutter_community_chat_interface/lib/src/service/chat_service.dart b/packages/flutter_community_chat_interface/lib/src/service/chat_service.dart index be0248f..e0e668b 100644 --- a/packages/flutter_community_chat_interface/lib/src/service/chat_service.dart +++ b/packages/flutter_community_chat_interface/lib/src/service/chat_service.dart @@ -1,7 +1,7 @@ import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; abstract class ChatService { - Stream> getChatsStream(); + Stream> getChatsStream(int pageSize); Future getChatByUser(ChatUserModel user); Future getChatById(String id); Future deleteChat(ChatModel chat); diff --git a/packages/flutter_community_chat_interface/lib/src/service/message_service.dart b/packages/flutter_community_chat_interface/lib/src/service/message_service.dart index ca84fbd..51fa50c 100644 --- a/packages/flutter_community_chat_interface/lib/src/service/message_service.dart +++ b/packages/flutter_community_chat_interface/lib/src/service/message_service.dart @@ -14,5 +14,6 @@ abstract class MessageService { Stream> getMessagesStream( ChatModel chat, + int pageSize, ); } diff --git a/packages/flutter_community_chat_interface/pubspec.yaml b/packages/flutter_community_chat_interface/pubspec.yaml index 039db8d..73753b7 100644 --- a/packages/flutter_community_chat_interface/pubspec.yaml +++ b/packages/flutter_community_chat_interface/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_community_chat_interface description: A new Flutter package project. -version: 0.6.0 +version: 1.0.0 publish_to: none environment: diff --git a/packages/flutter_community_chat_view/ios/Flutter/Generated.xcconfig b/packages/flutter_community_chat_view/ios/Flutter/Generated.xcconfig new file mode 100644 index 0000000..1167b5a --- /dev/null +++ b/packages/flutter_community_chat_view/ios/Flutter/Generated.xcconfig @@ -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 diff --git a/packages/flutter_community_chat_view/ios/Flutter/flutter_export_environment.sh b/packages/flutter_community_chat_view/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 0000000..8559eeb --- /dev/null +++ b/packages/flutter_community_chat_view/ios/Flutter/flutter_export_environment.sh @@ -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" diff --git a/packages/flutter_community_chat_view/ios/Podfile b/packages/flutter_community_chat_view/ios/Podfile new file mode 100644 index 0000000..ec43b51 --- /dev/null +++ b/packages/flutter_community_chat_view/ios/Podfile @@ -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 diff --git a/packages/flutter_community_chat_view/ios/Runner/GeneratedPluginRegistrant.h b/packages/flutter_community_chat_view/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000..7a89092 --- /dev/null +++ b/packages/flutter_community_chat_view/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/packages/flutter_community_chat_view/ios/Runner/GeneratedPluginRegistrant.m b/packages/flutter_community_chat_view/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000..76146f8 --- /dev/null +++ b/packages/flutter_community_chat_view/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,63 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#else +@import cloud_firestore; +#endif + +#if __has_include() +#import +#else +@import firebase_auth; +#endif + +#if __has_include() +#import +#else +@import firebase_core; +#endif + +#if __has_include() +#import +#else +@import firebase_storage; +#endif + +#if __has_include() +#import +#else +@import image_picker_ios; +#endif + +#if __has_include() +#import +#else +@import path_provider_foundation; +#endif + +#if __has_include() +#import +#else +@import sqflite; +#endif + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)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 diff --git a/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart b/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart index 1f7b324..e2b3792 100644 --- a/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart +++ b/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:typed_data'; 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/components/chat_bottom.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.chatUserService, required this.messageService, + required this.pageSize, this.translations = const ChatTranslations(), this.chat, this.onPressChatTitle, @@ -47,6 +49,7 @@ class ChatDetailScreen extends StatefulWidget { final ChatService service; final ChatUserService chatUserService; final MessageService messageService; + final int pageSize; @override State createState() => _ChatDetailScreenState(); @@ -58,6 +61,8 @@ class _ChatDetailScreenState extends State { Stream>? _chatMessages; ChatModel? chat; ChatUserModel? currentUser; + ScrollController controller = ScrollController(); + bool showIndicator = false; @override void initState() { @@ -65,7 +70,7 @@ class _ChatDetailScreenState extends State { // create a broadcast stream from the chat messages if (widget.chat != null) { _chatMessages = widget.messageService - .getMessagesStream(widget.chat!) + .getMessagesStream(widget.chat!, widget.pageSize) .asBroadcastStream(); } _chatMessagesSubscription = _chatMessages?.listen((event) { @@ -121,7 +126,6 @@ class _ChatDetailScreenState extends State { future: widget.service.getChatById(widget.chat?.id ?? ''), builder: (context, AsyncSnapshot snapshot) { var chatModel = snapshot.data; - return Scaffold( appBar: AppBar( centerTitle: true, @@ -186,11 +190,44 @@ class _ChatDetailScreenState extends State { ); previousMessage = message; } + return Listener( + onPointerMove: (event) { + var isTop = controller.position.pixels == + controller.position.maxScrollExtent; - return ListView( - reverse: true, - padding: const EdgeInsets.only(top: 24.0), - children: messageWidgets.reversed.toList(), + if (showIndicator == false && + isTop && + !(controller.position.userScrollDirection == + ScrollDirection.reverse)) { + setState(() { + showIndicator = true; + }); + _chatMessages = widget.messageService + .getMessagesStream(widget.chat!, widget.pageSize) + .asBroadcastStream(); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + showIndicator = false; + }); + } + }); + } + }, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + controller: controller, + reverse: true, + padding: const EdgeInsets.only(top: 24.0), + children: [ + ...messageWidgets.reversed.toList(), + if (snapshot.connectionState != + ConnectionState.active || + showIndicator) ...[ + const Center(child: CircularProgressIndicator()), + ], + ], + ), ); }, ), diff --git a/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart b/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart index d5542a4..b2e0097 100644 --- a/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart +++ b/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart @@ -4,7 +4,10 @@ // 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'; @@ -15,6 +18,7 @@ class ChatScreen extends StatefulWidget { required this.onPressChat, required this.onDeleteChat, required this.service, + required this.pageSize, this.onNoChats, this.deleteChatDialog, this.translations = const ChatTranslations(), @@ -25,10 +29,11 @@ class ChatScreen extends StatefulWidget { final ChatOptions options; final ChatTranslations translations; final ChatService service; - final VoidCallback? onPressStartChat; - final VoidCallback? onNoChats; + 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; @@ -42,6 +47,28 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final DateFormatter _dateFormatter = DateFormatter(); bool _hasCalledOnNoChats = false; + ScrollController controller = ScrollController(); + bool showIndicator = false; + Stream>? chats; + List 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) { @@ -72,145 +99,197 @@ class _ChatScreenState extends State { Column( children: [ Expanded( - child: ListView( - padding: const EdgeInsets.only(top: 15.0), - children: [ - StreamBuilder>( - stream: widget.service.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((_) { - widget.onNoChats!.call(); - }); - } - } else { - _hasCalledOnNoChats = - false; // Reset the flag if there are chats + 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; + }); } - return Column( - children: [ - for (ChatModel chat in snapshot.data ?? []) ...[ - Builder( - builder: (context) => !(widget - .disableDismissForPermanentChats && - !chat.canBeDeleted) - ? Dismissible( - confirmDismiss: (_) => - 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) + }); + } + }, + child: ListView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 15.0), + children: [ + StreamBuilder>( + 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( - translations - .deleteChatModalDescription, + chat.canBeDeleted + ? translations + .deleteChatModalTitle + : translations + .chatCantBeDeleted, style: const TextStyle( - fontSize: 16, + fontSize: 20, + fontWeight: + FontWeight.bold, ), ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - TextButton( - child: Text( - translations - .deleteChatModalCancel, - style: const TextStyle( - fontSize: 16, - ), + const SizedBox(height: 16), + if (chat.canBeDeleted) + Text( + translations + .deleteChatModalDescription, + style: const TextStyle( + fontSize: 16, ), - onPressed: () => - Navigator.of(context) - .pop(false), ), - if (chat.canBeDeleted) - ElevatedButton( - onPressed: () => - Navigator.of( - context, - ).pop(true), + const SizedBox(height: 16), + Row( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + TextButton( child: Text( translations - .deleteChatModalConfirm, + .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, ), ), ), - onDismissed: (_) => - 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( + key: ValueKey( + chat.id.toString(), + ), + child: ChatListItem( + widget: widget, + chat: chat, + translations: translations, + dateFormatter: _dateFormatter, + ), + ) + : 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, - widget.onPressStartChat!, + () async { + await widget.onPressStartChat!.call(); + getChats(); + }, translations, ), ], diff --git a/packages/flutter_community_chat_view/lib/src/screens/new_chat_screen.dart b/packages/flutter_community_chat_view/lib/src/screens/new_chat_screen.dart index 77fc819..80f0610 100644 --- a/packages/flutter_community_chat_view/lib/src/screens/new_chat_screen.dart +++ b/packages/flutter_community_chat_view/lib/src/screens/new_chat_screen.dart @@ -122,7 +122,9 @@ class _NewChatScreenState extends State { title: user.fullName ?? widget.translations.anonymousUser, ), ), - onTap: () => widget.onPressCreateChat(user), + onTap: () async { + await widget.onPressCreateChat(user); + }, ); }, ); diff --git a/packages/flutter_community_chat_view/pubspec.yaml b/packages/flutter_community_chat_view/pubspec.yaml index 0547797..ba3f64c 100644 --- a/packages/flutter_community_chat_view/pubspec.yaml +++ b/packages/flutter_community_chat_view/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_community_chat_view description: A standard flutter package. -version: 0.6.0 +version: 1.0.0 publish_to: none @@ -20,7 +20,7 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_community_chat path: packages/flutter_community_chat_interface - ref: 0.6.0 + ref: 1.0.0 cached_network_image: ^3.2.2 flutter_image_picker: git: