0

My app has chatting page. And I want to stream total number of unread messages. (sum of all unread messages in chatting rooms for specific user)

First I have to look up 'userChat' to get the list of chatKey the user is involved, then I should iterate through each 'messages/{chatKey}' to count number of unread. I have to get the number in real time, so both queries are written in .onValue.map((event) {...}).

My code is below.

Stream<int> numberOfTotalUnread() {
    int unread = 0;

    return _userChatDB.child(_uid).onValue.map((event) {
      unread = 0;
      if(event.snapshot.value != null) {
        for (var element in event.snapshot.children) {
          print('level one');
          print(element.key);

          _messageDB.child(element.key!).orderByChild('read').equalTo(false).onValue.map((ev) {
            if(ev.snapshot.value != null) {
              for (var el in ev.snapshot.children) {
                print('level two');
                print(el.key);
                if(Map<String, dynamic>.from(el.value as dynamic)['senderID'] != _uid) unread += 1;
              }
            }
          });
        }
      }
      print(unread);
      return unread;
    });
  }

But when I run it, I only see

I/flutter (31470): level one
I/flutter (31470): (some chat room key)
I/flutter (31470): 0

It never gets to level two and total unread is always zero. Is it possible to nest .onValue.map in flutter firebase? If so, what am I doing wrong?

EDITED

This is userChat/{userID}. It holds all the chat rooms info that user is participating.

{
  "{chatKey}" : {
    "id" : "ltRJxFIXzVPHhiCd4bYOTft5aKo2",
    "isMentor" : false,
    "lastMessage" : "99",
    "lastTime" : "2022-02-27 15:29:30.803526Z",
    "name" : "mento",
    "photoURL" : "https://firebasestorage.googleapis.com/~~",
  }
}

This is messages/{chatKey}. When message is first sent it has "read" : false field. And when the receiver sees it, that field is deleted.

My initial thought was since some messages don't have '''read''' field that might be the reason of error. But according to doc(https://firebase.google.com/docs/database/rest/retrieve-data#section-rest-ordered-data) there should be no problem. So I must be doing something wrong.

{
  "0VqSLAvcsCcx68H4" : {
    "message" : "6",
    "receiverID" : "68bCkh4p8FcM34VhQto4yJqNyNj2",
    "senderID" : "ltRJxFIXzVPHhiCd4bYOTft5aKo2",
    "time" : "2022-02-19 13:55:10.348445Z"
  },
  "0x7rp72Xgemq2T30" : {
    "message" : "2",
    "receiverID" : "68bCkh4p8FcM34VhQto4yJqNyNj2",
    "senderID" : "ltRJxFIXzVPHhiCd4bYOTft5aKo2",
    "time" : "2022-02-19 13:49:03.956939Z"
  },
  "19map5hhJTlqJwoN" : {
    "message" : "9",
    "receiverID" : "68bCkh4p8FcM34VhQto4yJqNyNj2",
    "senderID" : "ltRJxFIXzVPHhiCd4bYOTft5aKo2",
    "time" : "2022-02-12 13:42:23.180828Z"
  },
  "1LpD0qIW5dClPd4p" : {
    "message" : "7",
    "receiverID" : "68bCkh4p8FcM34VhQto4yJqNyNj2",
    "senderID" : "ltRJxFIXzVPHhiCd4bYOTft5aKo2",
    "time" : "2022-02-19 14:11:47.681230Z"
  },
}

EDITED2

I have another function that returns number of unread messages in single chat room. This one looks exactly same as the onValue.map statement in the inner side of numberOfTotalUnread(). This function works just fine even if only some (or no) messages has "read" property.

  Stream<int> numberOfUnread({required ChatModel chatModel}) {
    int unread = 0;
    
    return _messageDB.child(chatModel.key).orderByChild('read').equalTo(false).onValue.map((event) {
      unread = 0;
      if(event.snapshot.value != null) {
        event.snapshot.children.forEach((element) {
          if(Map<String, dynamic>.from(element.value as dynamic)['senderID'] != _uid) unread += 1;
        });
      }

      return unread;
    });
  }

EDITED 3

I figured out what I need is getting sum of Stream. This is the latest version(still not working).

Stream<int> numberOfTotalUnread() {
    int unread, unread2;
    List<Stream<int>> streams;

    return _userChatDB.child(_uid).onValue.map((event) {
      unread = 0;
      streams = [];

      for (var element in event.snapshot.children) {
        if(element.key != null) {
          print('level one');
          streams.add(_messageDB.child(element.key!).orderByChild('read').equalTo(false).onValue.map((ev) {
            print('level two');
            unread2 = 0;
            if(ev.snapshot.value != null) {
              for (var el in ev.snapshot.children) {
                print('level three');
                if(Map<String, dynamic>.from(el.value as dynamic)['senderID'] != _uid) unread2 += 1;
              }
            }
            return unread2;
          }));
        }
      }
      print(streams);

      for (var stream in streams) {
        stream.listen((content) {unread += content;});
      }
      return unread;
    });
  }
}

This is what I get.

I/flutter ( 5199): level one
I/flutter ( 5199): [Instance of '_MapStream<DatabaseEvent, int>']
I/flutter ( 5199): 0
I/flutter ( 5199): level two
I/flutter ( 5199): level three

So I got to the part of getting List of Stream, but my function returns 0 before getting the sum of stream contents. Any ideas?

Younghak Jang
  • 449
  • 3
  • 8
  • 22
  • Can you edit your question to show the data in the database at both `_userChatDB.child(_uid)` and `_messageDB.child(element.key!)` (as text, no screenshots please)? You can get this by clicking the "Export JSON" link in the overflow menu (⠇) on your [Firebase Database console](https://console.firebase.google.com/project/_/database/data). – Frank van Puffelen Feb 27 '22 at 15:53
  • @FrankvanPuffelen Could you check EDITED 3? I think what I need is to get Stream from a list of Stream. Put it another way, listen to multiple Stream and stream sum of their content. – Younghak Jang Mar 06 '22 at 14:27

2 Answers2

0

This is your query:

_messageDB.child(element.key!).orderByChild('read').equalTo(false).

So this looks for child nodes that have a property read with a value equal to false. In the JSON you show, none of the child nodes have a property read at all, so they won't be matched by this query.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • I actually copied small portion of my database. Rest of the part looks similar but I do have 'read' property in some of the messages. According to doc(https://firebase.google.com/docs/database/rest/retrieve-data#section-rest-ordered-data) I thought some fields with null value shouldn't matter. – Younghak Jang Feb 28 '22 at 15:39
  • A property with a `null` value is different from a non-existing property. – Frank van Puffelen Feb 28 '22 at 16:01
  • I deleted every messages in messages database, and then sent one new message. Now I have only one message with "read": false. Still I get unread = 0. I see 'level one' but no 'level two' in the console... – Younghak Jang Feb 28 '22 at 16:19
  • Edited my question. I have another function that has the same ```_messageDB.child(element.key!).orderByChild('read').equalTo(false).``` part but works fine even only some of the messages has "read" property. – Younghak Jang Feb 28 '22 at 16:35
0

This is the final working code. In essence, I started from Stream<List<Stream<int>>> and used Rx.combineLatest to make a Stream<Stream<int>>. Then I flattened stream layers to return Stream<int>.

It works, but let me know if there's more cleaner way.

  Stream<int> numberOfTotalUnread() {
    int unread;
    List<Stream<int>> streams = [];

    return flattenStreams<int>(_userChatDB.child(_uid).onValue.map((event) {
      streams = [];

      for (var element in event.snapshot.children) {
        if (element.key != null) {

          streams.add(_messageDB.child(element.key!).orderByChild('read').equalTo(false).onValue.map((ev) {
            unread = 0;
            if (ev.snapshot.value != null) {
              for (var el in ev.snapshot.children) {
                if (Map<String, dynamic>.from(
                    el.value as dynamic)['senderID'] != _uid) unread += 1;
              }
            }
            return unread;
          }));
        }
      }
      return Rx.combineLatest<int, int>(streams, (values) => values.reduce((a, b) => a + b));
    }));
  }

  Stream<T> flattenStreams<T>(Stream<Stream<T>> source) async* {
    await for (var stream in source) yield* stream;
  }
Younghak Jang
  • 449
  • 3
  • 8
  • 22