Text Channels
Message Grouping Algorithm
Within this algorithm, tail refers to whether the message omits displaying the user’s avatar / username.
This algorithm expects that the rendering of the output list is reversed, if you need it from oldest to newest: either reverse at the end or adapt the logic.
- Let L be the list of messages ordered from newest to oldest
- Let E be the list of elements to be rendered
- Let blockedMessages be a counter initialized to 0
- Let insertedUnreadDivider be a flag initialized to false
- Let lastReadId be the ID of the last read message (or “0” if none)
- For each message M in L:
- Let tail be true
- Let date be null
- Let next be the next item in list L
- If next is not null:
- Let adate and bdate be the times the message M and the next message were created respectively
- If adate and bdate are not the same day:
- Let date be adate
- Let tail be false if one of the following conditions is satisfied:
- Message M and next do not have the same author
- The difference between bdate and adate is equal to or over 7 minutes (420000ms)
- The masquerades for message M and next do not match
- Message M or next is a system message
- Message M replies to one or more messages
- next is before the last read message and the unread divider hasn’t been inserted yet
- Else if next is null:
- Let tail be false
- If the unread divider hasn’t been inserted and message M is before the last read message:
- Let insertedUnreadDivider be true
- Push an unread divider to list E
(type id: 1; cache key: true)
- If the author of message M has a “Blocked” relationship:
- Increment blockedMessages
- Else:
- If blockedMessages > 0:
- Push blocked message count to list E
(type id: 2; count: blockedMessages) - Reset blockedMessages to 0
- Push blocked message count to list E
- Push the message to list E
(type id: 0; cache key: message id:tail)
- If blockedMessages > 0:
- If date is not null:
- Push date formatted as “MMMM D, YYYY” to list E
(type id: 1; cache key: formatted date)
- Push date formatted as “MMMM D, YYYY” to list E
- If blockedMessages > 0 after processing all messages:
- Push blocked message count to list E
(type id: 2; count: blockedMessages)
- Push blocked message count to list E
- If the first element in E is an unread divider (type id: 1):
- Remove it from E (to avoid showing it alone at the bottom)
Element Type IDs
- Type 0: Message entry with tail flag and message content
- Type 1: Message divider (either date divider or unread divider)
- Type 2: Blocked messages counter showing the number of consecutive blocked messages
Note: the Stoat client also caches the objects produced for list E by pushing each object into a Map by their given cache key above, then retrieving them the next time the code is run OR creating a new object if one is not present. This prevents Solid.js from completely rebuilding the DOM whenever the message list updates.
Message View
The message view is finnicky but important to get right, below is the high-level specification:
- The message chunk size is 50.
- A skeleton of messages is displayed where no messages have been loaded and may appear. This includes before initial load, at the top of the list (if more can be loaded), and at the bottom of the list (if more can be loaded).
- If there are no more messages at the top of the channel, there is a conversation start indicator.
- On initial load, a chunk of messages is requested.
- Scroll position is sticky to the bottom of the message view.
- New messages are added to the end when at the bottom of the message view.
- Messages are updated when edited or deleted.
- Scroll position is stable when loading / unloading messages.
- Scrolling into the top or bottom skeletons should load more messages in that direction.
- Up to 150 messages should be rendered (on or off screen) at any given moment.
- Ability to jump and centre to a particular message / unread message.
- Ability to jump to the latest messages.
- On failure to load messages, there should be an exponential backoff.
- There should be locks & signals that prevent double-fetching and cancel unrelated fetches if a new fetch (e.g. up instead of down, end instead of up/down) is initiated.