Unread Messages

After chat’s first release, students ask for two more things:

  • When a room has messages a member has not read, Courselore must mark the room in the member’s list of rooms.
  • When a member opens a room, Courselore must show where the messages the member has not read start.

To meet these requirements, Courselore has to know what each member has read.

What a member has read

The first idea is a row for every message a member has read: a table of pairs (message_id, user_id). When Marcus reads a message, the server inserts a row with that message’s identifier and Marcus’s user identifier. A message is unread for Marcus if it has no row with Marcus’s user identifier.

In a chat room, that is a lot of rows. A team room collects about 5,000 messages in a semester. With five members, that is about 25,000 rows for one room. Every message a member reads is also one more write. When Marcus opens the team’s room after a weekend and reads 40 new messages, the server inserts 40 rows.

In a chat room, the messages form one list, and a member reads it from older to newer. So we can treat a member as having read every message up to some point. It is enough to store how far each member has read:

chat_read_positions
  room_id               integer  foreign key -> chat_rooms, not null
  user_id               integer  foreign key -> users, not null
  last_read_message_id  integer  foreign key -> chat_messages, not null
  primary key (room_id, user_id)

A row records the last message a member has read in a room. We call it the member’s read position in the room. A member has at most one read position per room. A message is unread for a member if its message_id is greater than the member’s last_read_message_id. This works because a room’s message identifiers increase in the order the messages were posted.

A team room with five members now has five rows, no matter how many messages it collects. When Marcus reads 40 new messages, the server moves Marcus’s read position forward with one update.

The row is created the first time the member opens the room. Until then, the member has no row for the room, and every message in it counts as unread.

Read positions in the API

The client sends the member’s read position to the server:

PUT /rooms/{room_id}/read-position

{
  "last_read_message_id": ...
}

For the first requirement, the client has to know which rooms have unread messages. When the client sends GET /courses/{course_id}/rooms for the list of rooms, each room in the response includes has_unread, which says whether the member has unread messages in it.

{
  "id": 12,
  "title": "Team 4",
  "has_unread": true
}

The server sets has_unread to true when the identifier of the room’s newest message is greater than the member’s last_read_message_id, or when the member has no read position in the room. The client shows a dot next to each room where has_unread is true.

For the second requirement, the client has to know where the unread messages start. When the client sends GET /rooms/{room_id} for one room, the response includes the member’s read position.

{
  "id": 12,
  "title": "Team 4",
  "last_read_message_id": 61842
}

The client shows the messages with identifiers greater than 61842 below a “New messages” line.