flutter_firebase_chat_core icon indicating copy to clipboard operation
flutter_firebase_chat_core copied to clipboard

Message stream pagination

Open mibrah42 opened this issue 3 years ago • 21 comments

The library does not seem to have pagination on the messages stream. Is this something that will be added?

mibrah42 avatar Apr 27 '21 03:04 mibrah42

Hi @mibrah42. Right now we are working on animations and pagination in the chat UI, but as of right now we don't have plans to add pagination to Firebase's messages stream. We are always open to PRs though :)

demchenkoalex avatar Apr 27 '21 07:04 demchenkoalex

So the pagination was added to the chat UI package, if there are volunteers to add pagination to Firebase we will be more than happy to accept a PR :)

demchenkoalex avatar Jul 14 '21 00:07 demchenkoalex

So the pagination was added to the chat UI package, if there are volunteers to add pagination to Firebase we will be more than happy to accept a PR :)

Does the documentation includes this new feature?

SalahAdDin avatar Sep 08 '21 07:09 SalahAdDin

@SalahAdDin Chat UI - yes, Firebase - no, feel free to add.

demchenkoalex avatar Sep 08 '21 08:09 demchenkoalex

I found this one! https://pub.dev/packages/paginate_firestore I think it could help.

holy-dev avatar Sep 11 '21 18:09 holy-dev

@demchenkoalex any news to support pagination to Firebase ?

azazadev avatar Sep 24 '21 12:09 azazadev

@azazadev this is the lowest priority we have, unless someone else do it, don't expect it this year. This package was designed as an option to quickly build an MVP and not a full-blown fully-featured chat backend.

demchenkoalex avatar Sep 24 '21 12:09 demchenkoalex

@demchenkoalex First thank you for the wonderful work! I try to add this feature in my project, reference official document and video. Here is my solution , you have done the most part of work in this, the rest is is easy piece in your package, big difference for the reads number which relate to the cost in real online project. Thank you again, excuse my bluntness and poor drawing skill. 截屏2021-10-12 上午9 40 57

Stream part

Stream<List<types.Message>> messages(types.Room room, [doc]) {
    return FirebaseFirestore.instance
        .collection('${config.roomsCollectionName}/${room.id}/messages')
        .orderBy('createdAt', descending: true)
        .endBeforeDocument(doc)
        .snapshots()

page part

Future fetchMessages(types.Room room,
      {var docWhere, int pageSize = 5}) async {
    List<QueryDocumentSnapshot<Map<String, dynamic>>>? quary;
    if (docWhere == null) {
//first bunch of messages, page1
      await FirebaseFirestore.instance
          .collection('${config.roomsCollectionName}/${room.id}/messages')
          .orderBy('createdAt', descending: true)
          .limit(pageSize)
          .get();
    } else {
//next page messages
      await FirebaseFirestore.instance
          .collection('${config.roomsCollectionName}/${room.id}/messages')
          .orderBy('createdAt', descending: true)
          .startAfterDocument(docWhere)
          .limit(pageSize)
          .get();
    }
    return quary;
  }

what2003 avatar Oct 12 '21 01:10 what2003

@demchenkoalex First thank you for the wonderful work! I try to add this feature in my project, reference official document and video. Here is my solution , you have done the most part of work in this, the rest is is easy piece in your package, big difference for the reads number which relate to the cost in real online project. Thank you again, excuse my bluntness and poor drawing skill. 截屏2021-10-12 上午9 40 57

Stream part

Stream<List<types.Message>> messages(types.Room room, [doc]) {
    return FirebaseFirestore.instance
        .collection('${config.roomsCollectionName}/${room.id}/messages')
        .orderBy('createdAt', descending: true)
        .endBeforeDocument(doc)
        .snapshots()

page part

Future fetchMessages(types.Room room,
      {var docWhere, int pageSize = 5}) async {
    List<QueryDocumentSnapshot<Map<String, dynamic>>>? quary;
    if (docWhere == null) {
//first bunch of messages, page1
      await FirebaseFirestore.instance
          .collection('${config.roomsCollectionName}/${room.id}/messages')
          .orderBy('createdAt', descending: true)
          .limit(pageSize)
          .get();
    } else {
//next page messages
      await FirebaseFirestore.instance
          .collection('${config.roomsCollectionName}/${room.id}/messages')
          .orderBy('createdAt', descending: true)
          .startAfterDocument(docWhere)
          .limit(pageSize)
          .get();
    }
    return quary;
  }

What is docWhere for?

SalahAdDin avatar Oct 12 '21 06:10 SalahAdDin

docWhere is just a name I made for my project, it is a doc which located at the last of List docs in page1, use it to tell firestore where next bunch of docs start.

what2003 avatar Oct 12 '21 11:10 what2003

@demchenkoalex First thank you for the wonderful work! I try to add this feature in my project, reference official document and video. Here is my solution , you have done the most part of work in this, the rest is is easy piece in your package, big difference for the reads number which relate to the cost in real online project. Thank you again, excuse my bluntness and poor drawing skill. 截屏2021-10-12 上午9 40 57

Stream part

Stream<List<types.Message>> messages(types.Room room, [doc]) {
    return FirebaseFirestore.instance
        .collection('${config.roomsCollectionName}/${room.id}/messages')
        .orderBy('createdAt', descending: true)
        .endBeforeDocument(doc)
        .snapshots()

page part

Future fetchMessages(types.Room room,
      {var docWhere, int pageSize = 5}) async {
    List<QueryDocumentSnapshot<Map<String, dynamic>>>? quary;
    if (docWhere == null) {
//first bunch of messages, page1
      await FirebaseFirestore.instance
          .collection('${config.roomsCollectionName}/${room.id}/messages')
          .orderBy('createdAt', descending: true)
          .limit(pageSize)
          .get();
    } else {
//next page messages
      await FirebaseFirestore.instance
          .collection('${config.roomsCollectionName}/${room.id}/messages')
          .orderBy('createdAt', descending: true)
          .startAfterDocument(docWhere)
          .limit(pageSize)
          .get();
    }
    return quary;
  }

Thanks @SalahAdDin , can you please give complete example ?

azazadev avatar Oct 12 '21 16:10 azazadev

Thanks @SalahAdDin , can you please give complete example ?

@azazadev You have to ask to @what2003, he did it.

SalahAdDin avatar Oct 13 '21 06:10 SalahAdDin

Thanks @SalahAdDin , can you please give complete example ?

@azazadev You have to ask to @what2003, he did it.

sorry, question to @what2003 any chance to have complete example ?

azazadev avatar Oct 13 '21 10:10 azazadev

My project with a lot custom feather is too complex to show, maybe later when I finish my project and organize my code to set an example. Of course the best way is waiting for @demchenkoalex to update. Again I shall express my gratitude for the brilliant work @demchenkoalex team did!

what2003 avatar Oct 13 '21 14:10 what2003

Hey @what2003 thanks for the kind words and an update that this is actually not that hard as I assume it would :D Definitely share an example if you will have a chance, in the meantime I will try to follow you leads to see if I could make it work too.

demchenkoalex avatar Oct 13 '21 14:10 demchenkoalex

It is my pleasure to contribute a little, example, made some changes base on @demchenkoalex flutter_firebase_chat_core example, hope it helps. My google-services.json is in it, so it can be directly built on android(not IOS), please ignore some Chinese characters in chat room and custom messages which appears as blank in this example. @azazadev

what2003 avatar Oct 14 '21 03:10 what2003

I tried to create pagination with firestore while taking inspiration from FilledStack's video about Flutter and Firestore real-time Pagination. The approach is not perfect but it's working fine for me.

"I tried it only with single-page chat room."

class MessagesService with ChangeNotifier {
  late int currentIndex;

  MessagesService();

  final databaseService = sl.get<DatabaseService>();

  ///constants
  static const _serverTime = 'metadata.createdAt';
  // static const _createdAt = 'createdAt';
  static const _resultLimit = 10;

  bool _hasMoreItems = true;
  bool _isLoading = false;
  bool get hasMoreItems => _hasMoreItems;
  bool get isLoading => _isLoading;

  void _setLoading(bool value) {
    _isLoading = value;
    // notifyListeners();
  }

  ///
  DocumentSnapshot? _lastDocument;

  final List<List<types.Message>> _allMessage = [];

  final _streamController = StreamController<List<types.Message>>.broadcast();

  Stream<List<types.Message>> messageStream() {
    requestData();
    return _streamController.stream;
  }

  void requestData() {
    if (_isLoading) return;
    _setLoading(true);
    Query<types.Message> query;

    /// First Query to get `_resultLimit` messages
    query = databaseService
        .messages()
        // .where(_serverTime, isGreaterThanOrEqualTo: timeStamp)
        .orderBy(_serverTime, descending: true)
        .limit(_resultLimit);

    currentIndex = _allMessage.length;

    /// If User scrolls to upwards
    if (_lastDocument != null) {
      if (_hasMoreItems) {
        query = query.startAfterDocument(_lastDocument!);
        query.snapshots().listen((snapshot) => _createList(snapshot, false));

        return;
      }
    }
    query.snapshots().listen((snapshot) => _createList(snapshot, true));
  }

  void _createList(QuerySnapshot<types.Message> snapshot, bool isNewMessage) {
    _setLoading(true);
    if (isNewMessage) {
// I'm using a hack here, this hack fixes a problem when user scroll upwards and a new mesasge came, the message doesn't appear. 
      currentIndex = 0;
    }

    print('Current Index $currentIndex');
    print('All Message Length ${_allMessage.length}');
    final tempList = snapshot.docs.map((e) => e.data()).toList();

    final pageExists = currentIndex < _allMessage.length;
    if (pageExists) {
      _allMessage[currentIndex] = tempList;
    } else {
      _allMessage.add(tempList);
    }
    final foldedList = _allMessage.fold<List<types.Message>>(<types.Message>[],
        (initialValue, element) => initialValue..addAll(element));
    _streamController.add(foldedList);

    if (currentIndex == _allMessage.length - 1) {
      _lastDocument = snapshot.docs.last;
    }

    // Determine if there's more Messages to request
    _hasMoreItems = tempList.length == _resultLimit;
    _setLoading(false);
  }

  @override
  void dispose() {
    super.dispose();
    _streamController.close();
  }
}

UsamaKarim avatar Nov 23 '21 10:11 UsamaKarim

I got pagination to work on my end by using FirestoreQueryBuilder from flutter fire ui package.

So I have this:

FirestoreQueryBuilder(
          query: FirebaseFirestore.instance
              .collection('rooms/${widget.room.id}/messages')
              .orderBy('createdAt', descending: true),
          
          builder: (context, snapshot, c) {
            if (snapshot.isFetching) {
              return const CircularProgressIndicator();
            }
          ...
         return Chat(
                    onEndReached: () {
                      snapshot.fetchMore();
                      return Future.value();
                    },
                   messages: snapshot.docs.map((e) {
                      final data = e.data();
                      if (data != null) {
                        var map = data as Map<dynamic, dynamic>;
                        var castedMap = map.cast<String, dynamic>();
                         final author = widget.room.users.firstWhere(
                          (u) => u.id == castedMap['authorId'],
                          orElse: () =>
                              types.User(id: castedMap['authorId'] as String),
                        );

                        data['author'] = author.toJson();
                        data['createdAt'] =
                            data['createdAt']?.millisecondsSinceEpoch;
                        data['id'] = e.id;
                        data['updatedAt'] =
                            data['updatedAt']?.millisecondsSinceEpoch;
                         return types.Message.fromJson(castedMap);
                      }

                      return types.Message.fromJson({});
                    }).toList(),
                   ....
             );
}

alexrabin avatar Feb 17 '22 01:02 alexrabin

It is my pleasure to contribute a little, example, made some changes base on @demchenkoalex flutter_firebase_chat_core example, hope it helps. My google-services.json is in it, so it can be directly built on android(not IOS), please ignore some Chinese characters in chat room and custom messages which appears as blank in this example. @azazadev

hey. your example is private I think

akshays-repo avatar Oct 21 '22 05:10 akshays-repo

It is my pleasure to contribute a little, example, made some changes base on @demchenkoalex flutter_firebase_chat_core example, hope it helps. My google-services.json is in it, so it can be directly built on android(not IOS), please ignore some Chinese characters in chat room and custom messages which appears as blank in this example. @azazadev

hey. your example is private I think

Or it was deleted.

SalahAdDin avatar Oct 21 '22 07:10 SalahAdDin

@akshays-repo @SalahAdDin My example included my firebase account info which is no longer in use, so please refer to my code snippet, else in my example just insignificant.

what2003 avatar Oct 21 '22 16:10 what2003