# Newly-Unread Indicator
A newly-unread badge is not the same as total unread messages.
Example product behavior:
- You receive messages from three unique people while away.
- The badge shows `3`.
- You open the messages screen.
- The badge clears, even if you did not open every conversation.
This is an acknowledgement indicator, not the canonical per-message read state.
## Read Path First
The app needs a cheap call:
```text
GET /me -> { newly_unread_count: 3 }
```
Near-real-time badge reads should not scan messages or replay Kafka events. Precompute the state.
## Redis Sorted Set Model
Use one sorted set per recipient:
```text
key = newly_unread:{recipient_user_id}
member = sender_user_id
score = latest_message_timestamp
```
Operations:
```text
ZADD newly_unread:B now A # A sent B a message
ZCARD newly_unread:B # badge count
ZREVRANGE newly_unread:B 0 9 # recent senders for display
DEL newly_unread:B # B acknowledged messages screen
```
Why sorted set:
- members are unique sender IDs,
- score keeps latest sender order,
- cardinality gives the badge count,
- a sender can update their timestamp without duplicating the count.
## Write Flow
```mermaid
sequenceDiagram
participant A as Sender A
participant API
participant DB as Message DB
participant R as Redis
participant WS as WebSocket Gateway
participant B as Recipient B
A->>API: send message to B
API->>DB: persist message
API->>R: ZADD newly_unread:B timestamp A
alt B online
API->>WS: push badge update
WS->>B: newly_unread_count changed
end
```
Persist the message first, then update the badge projection. If the projection update fails, it can be retried or rebuilt from recent messages.
## Acknowledgement Flow
```mermaid
sequenceDiagram
participant B as User B
participant API
participant R as Redis
B->>API: open messages screen
API->>R: DEL newly_unread:B
API-->>B: messages screen payload
```
This clears "newly unread" only. It does not necessarily mark all message threads read.
## Separate States
Do not confuse these:
| State | Meaning | Storage |
|---|---|---|
| Newly unread | Senders since last acknowledgement | Redis sorted set |
| Thread unread | Conversation has unread messages | message/thread read receipts |
| Message read | Specific message seen/read | message receipt table or per-thread cursor |
Product copy and API names should reflect the distinction.
## Failure Modes
| Failure | Effect | Mitigation |
|---|---|---|
| Redis loses key | badge undercounts | rebuild from recent messages or accept as ephemeral |
| Retry sends same event | duplicate badge risk | sorted set member uniqueness by sender ID |
| User opens messages while new message arrives | race around clearing | use acknowledgement timestamp or compare scores |
| WebSocket disconnected | badge stale until next poll | `/me` reconciliation |
| Sender changes display name | stale tooltip name | store IDs in Redis, fetch names from user/profile cache |
## Useful References
- [Redis sorted sets](https://redis.io/docs/latest/develop/data-types/sorted-sets/)
- [[wiki/realtime-database-and-websocket-scaling]]