added pagination

This commit is contained in:
mike doornenbal 2023-12-28 12:52:19 +01:00
parent 69bafc33e6
commit ac163a28f8
7 changed files with 278 additions and 105 deletions

View file

@ -22,9 +22,14 @@ List<GoRoute> getCommunityChatStoryRoutes(
options: configuration.chatOptionsBuilder(context), options: configuration.chatOptionsBuilder(context),
onNoChats: () async => onNoChats: () async =>
await context.push(CommunityChatUserStoryRoutes.newChatScreen), await context.push(CommunityChatUserStoryRoutes.newChatScreen),
onPressStartChat: () async => onPressStartChat: () async {
await configuration.onPressStartChat?.call() ?? if (configuration.onPressStartChat != null) {
await 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(

View file

@ -15,7 +15,7 @@ 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

View file

@ -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;
@ -25,6 +25,7 @@ class FirebaseMessageService implements MessageService {
List<ChatMessageModel> _cumulativeMessages = []; List<ChatMessageModel> _cumulativeMessages = [];
ChatModel? lastChat; ChatModel? lastChat;
int? chatPageSize; int? chatPageSize;
DateTime timestampToFilter = DateTime.now();
FirebaseMessageService({ FirebaseMessageService({
required ChatUserService userService, required ChatUserService userService,
@ -58,12 +59,21 @@ 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);
if (_cumulativeMessages.length == 1) {
lastMessage = await chatReference
.collection(
_options.messagesCollectionName,
)
.doc(newMessage.id)
.get();
}
var metadataReference = _db var metadataReference = _db
.collection( .collection(
_options.chatsMetaDataCollectionName, _options.chatsMetaDataCollectionName,
@ -188,14 +198,89 @@ class FirebaseMessageService implements MessageService {
} }
@override @override
Stream<List<ChatMessageModel>> getMessagesStream( Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) {
ChatModel chat, int pageSize) {
chatPageSize = pageSize;
_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)
.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
snapshot.data()!, snapshot.id),
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(),
);
var stream = query.snapshots();
// Subscribe to the stream and process the updates
_subscription = stream.listen((snapshot) async {
var messages = <ChatMessageModel>[];
for (var messageDoc in snapshot.docs) {
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);
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,
),
);
}
}
}
}
// 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: () { onCancel: () {
_subscription?.cancel(); _subscription?.cancel();
@ -203,7 +288,6 @@ class FirebaseMessageService implements MessageService {
debugPrint('Canceling messages stream'); debugPrint('Canceling messages stream');
}, },
); );
return _controller!.stream; return _controller!.stream;
} }
@ -258,7 +342,91 @@ class FirebaseMessageService implements MessageService {
messages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); 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;
}
} }

View file

@ -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,
@ -14,6 +15,9 @@ abstract class MessageService {
Stream<List<ChatMessageModel>> getMessagesStream( Stream<List<ChatMessageModel>> getMessagesStream(
ChatModel chat, ChatModel chat,
int pageSize,
); );
Future<void> fetchMoreMessage(int pageSize, ChatModel chat);
List<ChatMessageModel> getMessages();
} }

View file

@ -34,22 +34,19 @@ 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;
print(isNewDate);
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,10 +72,7 @@ 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: [

View file

@ -57,34 +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(); ScrollController controller = ScrollController();
bool showIndicator = false; 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!, widget.pageSize) 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
.fetchMoreMessage(widget.pageSize, widget.chat!);
}
}); });
if (event.isNotEmpty && }
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!);
@ -92,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();
} }
@ -170,66 +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) {
);
previousMessage = message;
}
return Listener(
onPointerMove: (event) {
var isTop = controller.position.pixels ==
controller.position.maxScrollExtent;
if (showIndicator == false &&
isTop &&
!(controller.position.userScrollDirection ==
ScrollDirection.reverse)) {
setState(() { setState(() {
showIndicator = true; showIndicator = false;
});
_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()),
],
],
),
);
}, },
child: ListView(
shrinkWrap: true,
physics: const AlwaysScrollableScrollPhysics(),
controller: controller,
reverse: true,
padding: const EdgeInsets.only(top: 24.0),
children: [
...detailRows,
if (showIndicator) ...[
const SizedBox(
height: 10,
),
const Center(child: CircularProgressIndicator()),
const SizedBox(
height: 10,
),
],
],
),
), ),
), ),
if (chatModel != null) if (chatModel != null)

View file

@ -29,8 +29,8 @@ class ChatScreen extends StatefulWidget {
final ChatOptions options; final ChatOptions options;
final ChatTranslations translations; final ChatTranslations translations;
final ChatService service; final ChatService service;
final Function? onPressStartChat; final Function()? onPressStartChat;
final Function? 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; final int pageSize;