flyerhq / flutter_firebase_chat_core

Actively maintained, community-driven Firebase BaaS for chat applications with an optional chat UI.
https://flyer.chat
Apache License 2.0
268 stars 212 forks source link

Message stream pagination #11

Open mibrah42 opened 3 years ago

mibrah42 commented 3 years ago

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

demchenkoalex commented 3 years ago

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 commented 3 years ago

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 :)

SalahAdDin commented 3 years ago

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?

demchenkoalex commented 3 years ago

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

holy-dev commented 3 years ago

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

azazadev commented 3 years ago

@demchenkoalex any news to support pagination to Firebase ?

demchenkoalex commented 3 years ago

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

what2003 commented 3 years ago

@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;
  }
SalahAdDin commented 3 years ago

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

what2003 commented 3 years ago

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.

azazadev commented 3 years ago

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

SalahAdDin commented 3 years ago

Thanks @SalahAdDin , can you please give complete example ?

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

azazadev commented 3 years ago

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 ?

what2003 commented 3 years ago

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!

demchenkoalex commented 3 years ago

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.

what2003 commented 3 years ago

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

UsamaKarim commented 2 years ago

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();
  }
}
alexrabin commented 2 years ago

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(),
                   ....
             );
}
akshays-repo commented 2 years ago

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

SalahAdDin commented 2 years ago

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.

what2003 commented 2 years ago

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