feat: update ChatRepositoryInterface with methods to manage pagination of messages in a chat

This commit is contained in:
Freek van de Ven 2025-02-13 15:29:09 +01:00 committed by FlutterJoey
parent 77d6f7257e
commit d475cf7298
7 changed files with 107 additions and 65 deletions

View file

@ -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

View file

@ -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<List<MessageModel>?> 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<void> 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<void> loadOldMessagesBefore({
required String userId,
required MessageModel firstMessage,
});
/// Send a message with the given parameters.
/// [chatId] is the chat id.
/// [senderId] is the sender id.

View file

@ -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<List<MessageModel>> _messageController =
BehaviorSubject<List<MessageModel>>();
final Map<String, int> _startIndexMap = {};
final Map<String, int> _endIndexMap = {};
static const int _chunkSize = 30;
@override
Future<void> createChat({
required List<String> users,
@ -110,60 +115,88 @@ class LocalChatRepository implements ChatRepositoryInterface {
Stream<List<MessageModel>?> 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<MessageModel>.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<MessageModel>()
.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<MessageModel>.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<void> loadNewMessagesAfter({
required String userId,
required MessageModel lastMessage,
}) async {
var allMessages = List<MessageModel>.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<void> loadOldMessagesBefore({
required String userId,
required MessageModel firstMessage,
}) async {
var allMessages = List<MessageModel>.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<MessageModel?> getMessage({
required String chatId,

View file

@ -135,14 +135,10 @@ class ChatService {
/// Returns a list of [MessageModel] stream.
Stream<List<MessageModel>?> 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.

View file

@ -94,15 +94,12 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
Stream<List<MessageModel>?> 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<void> loadNewMessagesAfter({
required String userId,
required MessageModel lastMessage,
}) async {}
@override
Future<void> loadOldMessagesBefore({
required String userId,
required MessageModel firstMessage,
}) async {}
}

View file

@ -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

View file

@ -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() ?? [];