From acbb809a57a7a8dbbc22a12d3f6aade2746a64ea Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 13 Feb 2025 15:29:09 +0100 Subject: [PATCH] feat: update ChatRepositoryInterface with methods to manage pagination of messages in a chat --- CHANGELOG.md | 1 + .../interfaces/chat_repostory_interface.dart | 18 ++- .../lib/src/local/local_chat_repository.dart | 125 +++++++++++------- .../lib/src/services/chat_service.dart | 4 - .../lib/src/firebase_chat_repository.dart | 15 ++- .../lib/src/config/chat_options.dart | 4 - .../chat_detail/chat_detail_screen.dart | 5 +- 7 files changed, 107 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e52f1..f609645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Added FlutterChatDetailNavigatorUserstory that can be used to start the userstory from the chat detail screen without having the chat overview screen - Changed the ChatDetailScreen to use the chatId instead of the ChatModel, the screen will now fetch the chat from the ChatService - Changed baseScreenBuilder to include a chatTitle that can be used to show provide the title logic to apps that use the baseScreenBuilder +- Added loadNewMessagesAfter, loadOldMessagesBefore and removed pagination from getMessages in the ChatRepositoryInterface to change pagination behavior to rely on the stream and two methods indicating that more messages should be added to the stream ## 4.0.0 - Move to the new user story architecture diff --git a/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart b/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart index d665633..dfa8872 100644 --- a/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart +++ b/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart @@ -43,16 +43,12 @@ abstract class ChatRepositoryInterface { /// Get the messages for the given [chatId]. /// Returns a list of [MessageModel] stream. - /// [pageSize] is the number of messages to be fetched. - /// [page] is the page number. /// [userId] is the user id. /// [chatId] is the chat id. /// Returns a list of [MessageModel] stream. Stream?> getMessages({ required String chatId, required String userId, - required int pageSize, - required int page, }); /// Get the message with the given [messageId]. @@ -63,6 +59,20 @@ abstract class ChatRepositoryInterface { required String messageId, }); + /// Signals that new messages should be loaded after the given message. + /// The stream should emit the new messages. + Future loadNewMessagesAfter({ + required String userId, + required MessageModel lastMessage, + }); + + /// Signals that old messages should be loaded before the given message. + /// The stream should emit the new messages. + Future loadOldMessagesBefore({ + required String userId, + required MessageModel firstMessage, + }); + /// Send a message with the given parameters. /// [chatId] is the chat id. /// [senderId] is the sender id. diff --git a/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart b/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart index 239dc64..44565c0 100644 --- a/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart +++ b/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart @@ -1,4 +1,5 @@ import "dart:async"; +import "dart:math" as math; import "dart:typed_data"; import "package:chat_repository_interface/chat_repository_interface.dart"; @@ -20,6 +21,10 @@ class LocalChatRepository implements ChatRepositoryInterface { final StreamController> _messageController = BehaviorSubject>(); + final Map _startIndexMap = {}; + final Map _endIndexMap = {}; + static const int _chunkSize = 30; + @override Future createChat({ required List users, @@ -110,60 +115,88 @@ class LocalChatRepository implements ChatRepositoryInterface { Stream?> getMessages({ required String chatId, required String userId, - required int pageSize, - required int page, }) { - ChatModel? chat; + var foundChat = + chats.firstWhereOrNull((chatModel) => chatModel.id == chatId); - chat = chats.firstWhereOrNull((e) => e.id == chatId); - - if (chat != null) { - var messages = List.from(chatMessages[chatId] ?? []); - - messages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - unawaited( - _messageController.stream.first - .timeout( - const Duration(seconds: 1), - ) - .then((oldMessages) { - var newMessages = messages.reversed - .skip(page * pageSize) - .take(pageSize) - .toList(growable: false) - .reversed - .toList(); - - if (newMessages.isEmpty) return; - - var allMessages = [...oldMessages, ...newMessages]; - - allMessages = allMessages - .toSet() - .toList() - .cast() - .toList(growable: false); - - allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - _messageController.add(allMessages); - }).onError((error, stackTrace) { - _messageController.add( - messages.reversed - .skip(page * pageSize) - .take(pageSize) - .toList(growable: false) - .reversed - .toList(), - ); - }), + if (foundChat == null) { + _messageController.add([]); + } else { + var allMessages = List.from( + chatMessages[chatId] ?? [], ); + allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + _startIndexMap[chatId] ??= math.max(0, allMessages.length - _chunkSize); + _endIndexMap[chatId] ??= allMessages.length; + + var displayedMessages = allMessages.sublist( + _startIndexMap[chatId]!, + _endIndexMap[chatId], + ); + _messageController.add(displayedMessages); } return _messageController.stream; } + @override + Future loadNewMessagesAfter({ + required String userId, + required MessageModel lastMessage, + }) async { + var allMessages = List.from( + chatMessages[lastMessage.chatId] ?? [], + )..sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + var lastMessageIndex = allMessages + .indexWhere((messageModel) => messageModel.id == lastMessage.id); + if (lastMessageIndex == -1) { + return; + } + + var currentEndIndex = + _endIndexMap[lastMessage.chatId] ?? allMessages.length; + _endIndexMap[lastMessage.chatId] = math.min( + allMessages.length, + currentEndIndex + _chunkSize, + ); + + var displayedMessages = allMessages.sublist( + _startIndexMap[lastMessage.chatId] ?? 0, + _endIndexMap[lastMessage.chatId], + ); + _messageController.add(displayedMessages); + } + + @override + Future loadOldMessagesBefore({ + required String userId, + required MessageModel firstMessage, + }) async { + var allMessages = List.from( + chatMessages[firstMessage.chatId] ?? [], + )..sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + var firstMessageIndex = allMessages + .indexWhere((messageModel) => messageModel.id == firstMessage.id); + if (firstMessageIndex == -1) { + return; + } + + var currentStartIndex = _startIndexMap[firstMessage.chatId] ?? 0; + _startIndexMap[firstMessage.chatId] = math.max( + 0, + currentStartIndex - _chunkSize, + ); + + var displayedMessages = allMessages.sublist( + _startIndexMap[firstMessage.chatId]!, + _endIndexMap[firstMessage.chatId] ?? allMessages.length, + ); + _messageController.add(displayedMessages); + } + @override Stream getMessage({ required String chatId, diff --git a/packages/chat_repository_interface/lib/src/services/chat_service.dart b/packages/chat_repository_interface/lib/src/services/chat_service.dart index 015f26f..739b38a 100644 --- a/packages/chat_repository_interface/lib/src/services/chat_service.dart +++ b/packages/chat_repository_interface/lib/src/services/chat_service.dart @@ -135,14 +135,10 @@ class ChatService { /// Returns a list of [MessageModel] stream. Stream?> getMessages({ required String chatId, - required int pageSize, - required int page, }) => chatRepository.getMessages( userId: userId, chatId: chatId, - pageSize: pageSize, - page: page, ); /// Send a message with the given parameters. diff --git a/packages/firebase_chat_repository/lib/src/firebase_chat_repository.dart b/packages/firebase_chat_repository/lib/src/firebase_chat_repository.dart index cbfafd5..56882cb 100644 --- a/packages/firebase_chat_repository/lib/src/firebase_chat_repository.dart +++ b/packages/firebase_chat_repository/lib/src/firebase_chat_repository.dart @@ -94,15 +94,12 @@ class FirebaseChatRepository implements ChatRepositoryInterface { Stream?> getMessages({ required String chatId, required String userId, - required int pageSize, - required int page, }) => _firestore .collection(_chatCollection) .doc(chatId) .collection(_messageCollection) .orderBy("timestamp") - .limit(pageSize) .snapshots() .map( (query) => query.docs @@ -199,4 +196,16 @@ class FirebaseChatRepository implements ChatRepositoryInterface { var snapshot = await uploadTask.whenComplete(() => {}); return snapshot.ref.getDownloadURL(); } + + @override + Future loadNewMessagesAfter({ + required String userId, + required MessageModel lastMessage, + }) async {} + + @override + Future loadOldMessagesBefore({ + required String userId, + required MessageModel firstMessage, + }) async {} } diff --git a/packages/flutter_chat/lib/src/config/chat_options.dart b/packages/flutter_chat/lib/src/config/chat_options.dart index 73ac248..ac86c0f 100644 --- a/packages/flutter_chat/lib/src/config/chat_options.dart +++ b/packages/flutter_chat/lib/src/config/chat_options.dart @@ -21,7 +21,6 @@ class ChatOptions { this.iconDisabledColor, this.chatAlignment, this.onNoChats, - this.pageSize = 20, ChatRepositoryInterface? chatRepository, UserRepositoryInterface? userRepository, }) : chatRepository = chatRepository ?? LocalChatRepository(), @@ -81,9 +80,6 @@ class ChatOptions { /// [onNoChats] is a function that is triggered when there are no chats. final Function? onNoChats; - - /// [pageSize] is the number of chats to load at a time. - final int pageSize; } /// Typedef for the chatTitleResolver function that is used to get a title for diff --git a/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart b/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart index 4f8496e..048f3f8 100644 --- a/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart @@ -226,7 +226,6 @@ class _Body extends HookWidget { var options = chatScope.options; var service = chatScope.service; - var pageSize = useState(chatScope.options.pageSize); var page = useState(0); var showIndicator = useState(false); var controller = useScrollController(); @@ -253,10 +252,8 @@ class _Body extends HookWidget { var messagesStream = useMemoized( () => service.getMessages( chatId: chat!.id, - pageSize: pageSize.value, - page: page.value, ), - [chat!.id, pageSize.value, page.value], + [chat!.id, page.value], ); var messagesSnapshot = useStream(messagesStream); var messages = messagesSnapshot.data?.reversed.toList() ?? [];