Tobias Fünke
Tobias Fünke

Reputation: 2024

How does slack store unread message counts?

I'm building a chat application and I'm trying to identify the state architecture for indicating unread messages.

My state currently contains a list of messages. On render, I group them by conversation so I can render a list of recipients and, for the selected recipient, the messages.

The simplest approach would be to also store a lastRead hash of {recipient: lastReadTimestamp} in the state as well. On render, count the number of messages in each conversation whose timestamp is greater than the stored lastRead timestamp to get the number of unread messages. And when the recipient is selected, set the lastRead timestamp for that recipient to that of the most recent message.

The problem with this is if, while you're away, 15+ messages come in - to the point that some are beyond the viewport and you'd have to scroll to see them. Once you select that recipient, it will mark the lastRead timestamp as the most recent message, essentially marking the entire conversation as "read" even though there are messages above that haven't been seen.

Instead I was hoping to have functionality more like Slack's. in-view or an InteractionObserver could detect when a message has actually come into the viewport.

slack screenshot

But what would I store in the state? The ids of each unread message? If so, when I refresh and my app receives all the messages, how does it know any of them are read? I could store the ids of each read message instead but that sounds unwieldy.

Has anyone seen a good design pattern for this?

Upvotes: 4

Views: 1575

Answers (1)

t1gor
t1gor

Reputation: 1292

We did use IntersectionObserver for exactly the same use case in a chat application.

Pre-requests: The messages have 2 meta-fields:

  • time (when it was sent)
  • timeSeen (when it was viewed)

The flow is as follows:

  • clientA sends a message via the server
  • server appends time = now() and timeSeen = null to that message
  • server saves the messages to the databases
  • server forwards the message to clientB
  • clientB checks that the message does not have timeSeen, e.g. it's null
  • clientB renders the message inside an IntersectionObserver wrapped component, which triggers a hook when message is in viewport and passes the messageId as a param
  • clientB sends a request to the server to mark the message as seen when requested by the hook
  • server marks the message with messageId as seen by setting timeSeen = now()
  • server sends an update to clientB with meta into on the message with messageId (timeSeen updated)
  • clientB sees that timeSeen is not null anymore and does not call the hook on re-renders

I know this sounds a bit of an overkill, but this way you can reliably get the following:

  • All messages will have meta on when those where seen
  • You can aggregate the unseen counters and lists on the server side based on the meta

Upvotes: 1

Related Questions