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 6652ad5..9fc0052 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 @@ -22,9 +22,14 @@ List getCommunityChatStoryRoutes( options: configuration.chatOptionsBuilder(context), onNoChats: () async => await context.push(CommunityChatUserStoryRoutes.newChatScreen), - onPressStartChat: () async => - await configuration.onPressStartChat?.call() ?? - await context.push(CommunityChatUserStoryRoutes.newChatScreen), + onPressStartChat: () async { + if (configuration.onPressStartChat != null) { + return await configuration.onPressStartChat?.call(); + } + + return await context + .push(CommunityChatUserStoryRoutes.newChatScreen); + }, onPressChat: (chat) => configuration.onPressChat?.call(context, chat) ?? context.push( diff --git a/packages/flutter_community_chat/pubspec.yaml b/packages/flutter_community_chat/pubspec.yaml index 5aa1391..ca93837 100644 --- a/packages/flutter_community_chat/pubspec.yaml +++ b/packages/flutter_community_chat/pubspec.yaml @@ -15,7 +15,7 @@ environment: dependencies: flutter: sdk: flutter - go_router: ^12.1.1 + go_router: any flutter_community_chat_view: git: url: https://github.com/Iconica-Development/flutter_community_chat 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 2c982d2..6ad72c8 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 @@ -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:uuid/uuid.dart'; -class FirebaseMessageService implements MessageService { +class FirebaseMessageService with ChangeNotifier implements MessageService { late final FirebaseFirestore _db; late final FirebaseStorage _storage; late final ChatUserService _userService; @@ -25,6 +25,7 @@ class FirebaseMessageService implements MessageService { List _cumulativeMessages = []; ChatModel? lastChat; int? chatPageSize; + DateTime timestampToFilter = DateTime.now(); FirebaseMessageService({ required ChatUserService userService, @@ -58,12 +59,21 @@ class FirebaseMessageService implements MessageService { ) .doc(chat.id); - await chatReference + var newMessage = await chatReference .collection( _options.messagesCollectionName, ) .add(message); + if (_cumulativeMessages.length == 1) { + lastMessage = await chatReference + .collection( + _options.messagesCollectionName, + ) + .doc(newMessage.id) + .get(); + } + var metadataReference = _db .collection( _options.chatsMetaDataCollectionName, @@ -188,14 +198,89 @@ class FirebaseMessageService implements MessageService { } @override - Stream> getMessagesStream( - ChatModel chat, int pageSize) { - chatPageSize = pageSize; + Stream> getMessagesStream(ChatModel chat) { _controller = StreamController>( onListen: () { - if (chat.id != null) { - _subscription = _startListeningForMessages(chat); - } + var messagesCollection = _db + .collection(_options.chatsCollectionName) + .doc(chat.id) + .collection(_options.messagesCollectionName) + .withConverter( + fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson( + snapshot.data()!, snapshot.id), + toFirestore: (user, _) => user.toJson(), + ); + var query = messagesCollection + .where( + 'timestamp', + isGreaterThan: timestampToFilter, + ) + .withConverter( + 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 = []; + + 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 uniqueObjects = + _cumulativeMessages.toSet().toList(); + _cumulativeMessages = uniqueObjects; + _cumulativeMessages + .sort((a, b) => a.timestamp.compareTo(b.timestamp)); + notifyListeners(); + timestampToFilter = DateTime.now(); + }); }, onCancel: () { _subscription?.cancel(); @@ -203,7 +288,6 @@ class FirebaseMessageService implements MessageService { debugPrint('Canceling messages stream'); }, ); - return _controller!.stream; } @@ -258,7 +342,91 @@ class FirebaseMessageService implements MessageService { messages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); _controller?.add(messages); + notifyListeners(); }, ); } + + @override + Future 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 messages = []; + QuerySnapshot? 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( + 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( + fromFirestore: (snapshot, _) => + FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id), + toFirestore: (user, _) => user.toJson(), + ) + .get(); + if (messagesQuerySnapshot.docs.isNotEmpty) { + lastMessage = messagesQuerySnapshot.docs.last; + } + } + + List messageDocuments = messagesQuerySnapshot.docs + .map((QueryDocumentSnapshot 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 getMessages() { + return _cumulativeMessages; + } } 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 51fa50c..d317b17 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 @@ -1,7 +1,8 @@ import 'dart:typed_data'; +import 'package:flutter/material.dart'; import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; -abstract class MessageService { +abstract class MessageService with ChangeNotifier { Future sendTextMessage({ required ChatModel chat, required String text, @@ -14,6 +15,9 @@ abstract class MessageService { Stream> getMessagesStream( ChatModel chat, - int pageSize, ); + + Future fetchMoreMessage(int pageSize, ChatModel chat); + + List getMessages(); } diff --git a/packages/flutter_community_chat_view/lib/src/components/chat_detail_row.dart b/packages/flutter_community_chat_view/lib/src/components/chat_detail_row.dart index e4d2619..eacf2ee 100644 --- a/packages/flutter_community_chat_view/lib/src/components/chat_detail_row.dart +++ b/packages/flutter_community_chat_view/lib/src/components/chat_detail_row.dart @@ -34,22 +34,19 @@ class _ChatDetailRowState extends State { @override Widget build(BuildContext context) { 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( padding: EdgeInsets.only( - top: isNewDate || - widget.previousMessage == null || - widget.previousMessage?.sender.id != widget.message.sender.id - ? 25.0 - : 0, + top: isNewDate || isSameSender ? 25.0 : 0, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isNewDate || - widget.previousMessage == null || - widget.previousMessage?.sender.id != - widget.message.sender.id) ...[ + if (isNewDate || isSameSender) ...[ Padding( padding: const EdgeInsets.only(left: 10.0), child: widget.message.sender.imageUrl != null && @@ -75,10 +72,7 @@ class _ChatDetailRowState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - if (isNewDate || - widget.previousMessage == null || - widget.previousMessage?.sender.id != - widget.message.sender.id) + if (isNewDate || isSameSender) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ 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 e2b3792..26f6ae5 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 @@ -57,34 +57,29 @@ class ChatDetailScreen extends StatefulWidget { class _ChatDetailScreenState extends State { // stream listener that needs to be disposed later - StreamSubscription>? _chatMessagesSubscription; - Stream>? _chatMessages; - ChatModel? chat; ChatUserModel? currentUser; ScrollController controller = ScrollController(); bool showIndicator = false; + late MessageService messageSubscription; + Stream>? stream; + ChatMessageModel? previousMessage; + List detailRows = []; @override void initState() { super.initState(); - // create a broadcast stream from the chat messages + messageSubscription = widget.messageService; + messageSubscription.addListener(onListen); if (widget.chat != null) { - _chatMessages = widget.messageService - .getMessagesStream(widget.chat!, widget.pageSize) - .asBroadcastStream(); - } - _chatMessagesSubscription = _chatMessages?.listen((event) { - // check if the last message is from the current user - // if so, set the chat to read + stream = widget.messageService.getMessagesStream(widget.chat!); + stream?.listen((event) {}); 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((_) { if (widget.chat != null) { widget.onReadChat(widget.chat!); @@ -92,9 +87,34 @@ class _ChatDetailScreenState extends State { }); } + 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 void dispose() { - _chatMessagesSubscription?.cancel(); + messageSubscription.removeListener(onListen); super.dispose(); } @@ -170,66 +190,48 @@ class _ChatDetailScreenState extends State { body: Column( children: [ Expanded( - child: StreamBuilder>( - stream: _chatMessages, - builder: (context, snapshot) { - var messages = snapshot.data ?? chatModel?.messages ?? []; - ChatMessageModel? previousMessage; + child: Listener( + onPointerMove: (event) async { + var isTop = controller.position.pixels == + controller.position.maxScrollExtent; - var messageWidgets = []; - - for (var message in messages) { - messageWidgets.add( - ChatDetailRow( - previousMessage: previousMessage, - showTime: widget.showTime, - translations: widget.translations, - message: message, - userAvatarBuilder: widget.options.userAvatarBuilder, - ), - ); - previousMessage = message; - } - return Listener( - onPointerMove: (event) { - var isTop = controller.position.pixels == - controller.position.maxScrollExtent; - - if (showIndicator == false && - isTop && - !(controller.position.userScrollDirection == - ScrollDirection.reverse)) { + if (showIndicator == false && + !isTop && + controller.position.userScrollDirection == + ScrollDirection.reverse) { + setState(() { + showIndicator = true; + }); + await widget.messageService + .fetchMoreMessage(widget.pageSize, widget.chat!); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { setState(() { - showIndicator = true; - }); - _chatMessages = widget.messageService - .getMessagesStream(widget.chat!, widget.pageSize) - .asBroadcastStream(); - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - showIndicator = false; - }); - } + 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) 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 b2e0097..2eb1e87 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 @@ -29,8 +29,8 @@ class ChatScreen extends StatefulWidget { final ChatOptions options; final ChatTranslations translations; final ChatService service; - final Function? onPressStartChat; - final Function? onNoChats; + final Function()? onPressStartChat; + final Function()? onNoChats; final void Function(ChatModel chat) onDeleteChat; final void Function(ChatModel chat) onPressChat; final int pageSize;