3

Edit 2

This is my current setup. I am removing the observer when the side navigation view hides and restart it when it appears. This would be unnecessary if user defaults would return a fresh value in queryStarting(atValue: defaults.double(forKey: lastReadMessageTimestampKey) + 0.000001) like timeIntervalSince1970 does in a similar query elsewhere: queryStarting(atValue: NSDate().timeIntervalSince1970).

class SideNavigationController: UIViewController {
    
    ...

    var unreadMessageCountUpdateQueryHandle: DatabaseHandle?

    ...
    
    override func viewWillAppear(_ animated: Bool) {
        logger.info("SideNavigationControllers view will appear")

        updateUnreadMessagesCount()

        super.viewWillAppear(true)
        
        ...
    }


    override func viewWillDisappear(_ animated: Bool) {
        logger.info("SideNavigationControllers view will disapear")
        
        stopUpdatingUnreadMessagesCount()
        
        super.viewWillDisappear(animated)
        
        ...
    }

    func updateUnreadMessagesCount() {
        logger.info("Updating count of unread messages")

        let messageTimestampKey = DiscussionMessage.CodingKeys.messageTimestamp.stringValue
        let defaults = UserDefaults.standard
      
        unreadMessageCountUpdateQueryHandle = messagesReference.queryOrdered(byChild: messageTimestampKey).queryStarting(atValue: defaults.double(forKey: lastReadMessageTimestampKey) + 0.000001).observe(.value) { (snapshot) in
            if let value = snapshot.value as? [String: Any] {
                print("Number of unread messages: \(value.count) Last Read TS \(UserDefaults.standard.double(forKey: lastReadMessageTimestampKey))")
                self.menuItems[1].itemName = "Discussions (\(value.count))"
                self.sideNavTableView.reloadData()
            } else {
                self.menuItems[1].itemName = "Discussions"
                self.sideNavTableView.reloadData()
            }
        }
    }
    
    func stopUpdatingUnreadMessagesCount() {
        guard let handle = unreadMessageCountUpdateQueryHandle else { return }
        messagesReference.removeObserver(withHandle: handle)
    }

Edit:

Adding .synchronize() after saving the default value is not helping either.

I have also tweaked the query a little bit to start with an offset from the last read message's timestamp so that it won't be included when I count unread messages.

Now the query looks like this:

let discussionMessageTimestampKey = DiscussionMessage.CodingKeys.messageTimestamp.stringValue
messagesReference.queryOrdered(byChild: discussionMessageTimestampKey)
.queryStarting(atValue: UserDefaults.standard.double(forKey: lastReadMessageTimestampKey) + 0.000001)
.observe(.value) { (snapshot) in
    if let value = snapshot.value as? [String: Any] {
        print("Number of unread messages: \(value.count) Last Read TS \(UserDefaults.standard.double(forKey: lastReadMessageTimestampKey))")
    }
}

With this query initially the unread messages count is 0 and then it becomes 1, 2, 3, etc. as new messages come in. This is even after I open the chat view and the last read message timestamp gets updated inside the query. I know that the last read message timestamp is updating as the console output looks like this:

Number of unread messages: 1 Last Read TS TS_OF_SECOND_LAST_MESSAGE
Number of unread messages: 2 Last Read TS TS_OF_SECOND_LAST_MESSAGE
Number of unread messages: 3 Last Read TS TS_OF_SECOND_LAST_MESSAGE

And my database structure looks like this for people who are curious:

speech drill db structure 1 speech drill db structure 2

Another way to do this might involve including 2 queries. Then the first query's execution could be stopped when the view is disappearing. But I don't know how to stop a query execution and could not find any documentation for it.

let messageTimestampKey = DiscussionMessage.CodingKeys.messageTimestamp.stringValue
let defaults = UserDefaults.standard
let query = messagesReference.observe(.childAdded) { (snapshot) in
    if snapshot.exists() {
        let startTimestamp = defaults.double(forKey: lastReadMessageTimestampKey)
        let offset: Double = 0.00000001
        messagesReference.queryOrdered(byChild: messageTimestampKey).queryStarting(atValue: startTimestamp + offset).observe(.value) { (snapshot) in
            if let value = snapshot.value as? [String: Any] {
                print("Number of unread messages: \(value.count) Last Read TS \(UserDefaults.standard.double(forKey: lastReadMessageTimestampKey))")
            }
        }
    }
}

I am creating a chat app and I want to count the number of unread messages. The value of the last read timestamp is saved at a location in my realtime database and each message has a timestamp attached to it. The last read message's timestamp is also stored in user defaults. So I tried using that value with queryStarting(atValue:). But it is not working as expected. Even after the default's value gets updated, the query still uses an older value, and unread message count keeps going up with .value event. With .childAdded event, I keep getting a constant (always incorrect) count even when new messages are added.

let discussionMessageTimestampKey = DiscussionMessage.CodingKeys.messageTimestamp.stringValue
messagesReference.queryOrdered(byChild: discussionMessageTimestampKey).queryStarting(atValue: UserDefaults.standard.double(forKey: lastReadMessageTimestampKey)).observe(.value) { (snapshot) in
    if let value = snapshot.value as? [String: Any] {
        print("Number of unread messages: \(value.count) Last Read TS \(UserDefaults.standard.double(forKey: lastReadMessageTimestampKey))")
    }
}

I am using a similar query for firing .childAdded event only for newly added messages and not deleted messages. Here I have a strong feeling that NSDate().timeIntervalSince1970 keeps returning a fresh value whenever a child is added or deleted. As even if 4 messages are added after the query was created and 1 of them was deleted, this event is not triggered. As it is designed to be triggered as explained by Frank here.

let discussionMessageTimestampKey = DiscussionMessage.CodingKeys.messageTimestamp.stringValue
messagesReference.queryOrdered(byChild: discussionMessageTimestampKey).queryStarting(atValue: NSDate().timeIntervalSince1970).observe(.childAdded) { 
    (snapshot) in
    ...
}

This brings me to my question. Since the user defaults query is not working, and the last read message timestamp is also stored in firebase. Can we maybe use that value as queryStarting(atValue:) instead (as I am hoping it will return a fresh value like NSDate().timeIntervalSince1970 does). The problem here is that both the last read message timestamp and the number of messages can change. Hence if I create an observer on one, then it will lead to unnecessary data loading at another end. Could someone explain to me why the query works as expected with NSDate().timeIntervalSince1970 but not with UserDefaults? And how I can fix this issue of counting number of unread messages optimally.

(One method I thought of involved adding a KVO on user defaults. But I saw that process also has complications so I did not go down that route.)

Parth
  • 2,682
  • 1
  • 20
  • 39

2 Answers2

0

There are few solution I can think of for this issue. I am not sure how is your data structure on Firebase. I assume you will have a list of messages and user node.

If your goal is to count unread message.

You can try changing your approach. When you get new message from .childAdded you might want store it into respective user node and remove it when you trigger read action on specific message. It's more efficient this way, especially when you have larger message later on. Or instead of adding a new node just update the value of totalUnreadMessage in your user node

Your node should look similar like this

root
|
- users
  |
  - 0
    |
    - totalUnreadMessage : 10  << update this when there is new message for this user

Additionally regarding your query, why it doesn't work when you use UserDefaults, most likely because the data type mismatch. eg: You are storing Date which is an object and trying to retrieve double value.

If you plan to use the last read message timestamp, I feel it won't be as efficient as adjusting how you store or calculate total unread message as above.

usergio2
  • 469
  • 3
  • 12
  • I don't think mismatch is an issue. The query works as expected on the first run. Also my question is, if it works for `NSDate().timeIntervalSince1970` then it should work for something else too. Which leads me to believe that `NSDate().timeIntervalSince1970` keep returning new value where as `UserDefaults` does not. – Parth Feb 19 '21 at 03:36
  • @ParthTamane I see, I misunderstood. Yes you are right `NSDate().timeIntervalSince1970` will always return a new value. `UserDefault` wont' because it store the value, so it will return the exact same value as what you store. – usergio2 Feb 19 '21 at 03:42
  • Yes, but I am updating that value in a different place. – Parth Feb 19 '21 at 04:05
0

I had similar issues with offline persistence enabled

Database.database().isPersistenceEnabled = true

try to disable it by changing this line to

Database.database().isPersistenceEnabled = false

ossamacpp
  • 656
  • 5
  • 11