Question
Should a member's last-read position be visible to other people in a River room, the way Matrix and WhatsApp show read receipts? And if so, should the room owner control whether reads are public?
Raised by @sanity on 2026-07-23 alongside the jump-to-unread request (#459). Filing the analysis so the decision is recorded rather than re-litigated later.
Recommendation: keep read position private, and do not add public receipts
Five reasons, roughly in order of weight.
1. The reference class is group chat, and group chat does not do this
Read receipts are a one-to-one and small-group messenger feature. The closest analogs to River have never had them:
|
Resume reading |
Public receipts |
| Slack |
Unread divider, jump to first unread, mark as unread, Catch Up |
None |
| Discord |
Red "New Messages" line, Mark As Read, unread dot separate from mention badge |
None |
| IRC / IRCv3 |
The original last-read line, synced across devices (read-marker spec) |
None, explicitly private-only |
| Matrix / Element |
m.fully_read marker, jump to first unread |
m.read, avatar stack |
| WhatsApp, iMessage, Signal, Telegram |
Badge count |
Yes |
Every system in the top group invested in the resume-reading side instead. River is community chat, so that is the group it belongs to.
2. Matrix separates the two concepts, and deliberately keeps receipts out of persistent history
Matrix is the one system that does both, and it is worth being precise about how. m.fully_read is a private bookmark; m.read is a public receipt. They are distinct object types in the client-server API. Receipts are delivered as ephemeral data (EDUs) and never enter the room's persistent event DAG. The rationale appears in MSC2285: EDUs "have no reference point in the DAG, meaning servers won't agree on whether the EDU is authorised". High-frequency, low-stakes UI state does not justify permanent, federated, cryptographically-ordered history.
River has no ephemeral channel. Contract state is the only transport we have. So a River implementation would land in exactly the place Matrix refused to put it, and would be strictly worse along every axis: permanently stored, individually signed, replicated to every subscriber, CRDT-merged on every change, and in a public room readable by anyone holding the room key, including hosting peers who are not members of the room at all. Matrix receipts are at least scoped to member-facing homeservers.
3. The client cost lands on three axes simultaneously
PR #458 (merged 2026-07-23) fixed mobile jank traced to an O(history) use_memo re-running over the whole message list on every ROOMS mutation. Receipts in contract state would re-create that pressure from a different direction:
- State churn. Every member's read-position update is a contract state change, broadcast to all subscribers, mutating
ROOMS on every client and re-running every subscribed memo. The mutation rate would scale with the number of people actively reading, which is the worst possible scaling for the pathology we just fixed.
- Viewport tracking. Publishing your own position accurately requires knowing which messages are actually on screen. River currently has one
IntersectionObserver, on a bottom sentinel (ui/src/components/conversation.rs:1160-1220). Per-message observers on mobile are a real cost.
- Render. Showing who read up to a given point needs a members-by-message index rebuilt whenever any receipt changes.
max_members defaults to 200 (common/src/room_state/configuration.rs).
Plus the network cost: each read update is a signed CRDT delta broadcast to every subscriber, immediately after the 0.2.105 work on update-broadcast load.
4. Owner-controlled receipts have no real precedent, and could not be enforced here
I could not find a consumer app where a specific group's creator toggles receipts for that group's members. The closest real example is Microsoft Teams, where an IT administrator sets an org-wide messaging policy to "User controlled", "On for everyone" or "Off for everyone", and users cannot override the latter two (Microsoft support). That is tenant-level and server-enforced, which is a different thing from a room owner's preference.
More importantly, River could not enforce such a flag. Room state is public and clients are user-controlled, so a modified client can publish receipts against the owner's setting, or withhold them despite it. The flag would be advisory, which is the weakest possible form for a privacy control: it looks like a guarantee and is not one.
5. The social cost is documented, and the industry response has been to add opt-outs
Survey research on read receipts (Lynden and Rasmussen, Copenhagen, n=108) reports social anxiety, senders over-checking, receivers avoiding opening messages, and roughly a third of respondents reporting feeling ignored. Every platform's response has been to add escape hatches: the Signal, WhatsApp and iMessage toggles, and Matrix's private-receipt MSC. None have added more receipts.
Worth noting that WhatsApp groups always show receipts with no per-member opt-out, and that is the specific case people complain about. Owner-mandated receipts would have the same shape.
The real gap is cross-device, and it should be solved privately
The limitation users actually hit is not that others cannot see their position. It is that they lose their own position when moving between devices.
Delegate secrets live on the node, not the device (delegates/chat-delegate/, origin-partitioned key/value store). Two devices pointed at the same gateway share the marker; two local nodes do not. So read on your phone against a local node, open your laptop against a different node, and you start over.
Matrix hit this exact problem and answered it without publishing anything: MSC2285 added m.read.private, a receipt that syncs your own unread state across your own devices and never reaches other users.
The River equivalent is the mergeable private delegate data direction @sanity floated on 2026-07-05: let delegates optionally expose a merge operation so private data folds together across a user's peers. A last-read pointer is close to an ideal pilot for that:
- The payload is a few bytes.
- The merge is "take the further position", a max over a total order, so it is a join-semilattice and trivially commutative. No conflict handling, no ordering subtleties.
- It has an obvious correctness test and a visible user benefit.
That is a much gentler first case than secrets with key and DEK reconciliation across replicas.
If we decide to build receipts anyway
Feasibility is not the constraint. MemberInfoV1 (common/src/room_state/member_info.rs) is already exactly the required pattern: each member writes a self-signed entry, one record per member, higher version wins with a signature tiebreak, entries pruned when the member is pruned, BTreeMap summaries for deterministic anti-entropy. A LastReadV1 would mirror it almost line for line. That ease is the risk, not the argument in favour.
If we do it, the constraints that follow from the above:
- Per-member opt-in, not owner-mandated. Owner-mandated is the WhatsApp-groups design that draws the complaints.
- Coarse resolution. Something like "active today" rather than a per-message position, which cuts both the churn and the social pressure.
- Off the message hot path, so a receipt update never triggers a full conversation re-render.
- Set expectations honestly in the UI: in a public room this is world-readable, not room-readable.
Caveats on the evidence
- Telegram's current small-group receipt threshold is unconfirmed. Secondary sources disagree (50 at launch, later 100), and I could not find an official statement. The 7-day expiry is confirmed.
- The "WhatsApp groups always show receipts" claim rests on secondary sources; WhatsApp's own FAQ page was not reachable during this survey.
- I have not audited how much of a private room's state River encrypts. That would change the exposure story for private rooms specifically, though not for public ones.
Related: #459 (the resume-reading feature this came from), #446, #70
[AI-assisted - Claude]
Question
Should a member's last-read position be visible to other people in a River room, the way Matrix and WhatsApp show read receipts? And if so, should the room owner control whether reads are public?
Raised by @sanity on 2026-07-23 alongside the jump-to-unread request (#459). Filing the analysis so the decision is recorded rather than re-litigated later.
Recommendation: keep read position private, and do not add public receipts
Five reasons, roughly in order of weight.
1. The reference class is group chat, and group chat does not do this
Read receipts are a one-to-one and small-group messenger feature. The closest analogs to River have never had them:
m.fully_readmarker, jump to first unreadm.read, avatar stackEvery system in the top group invested in the resume-reading side instead. River is community chat, so that is the group it belongs to.
2. Matrix separates the two concepts, and deliberately keeps receipts out of persistent history
Matrix is the one system that does both, and it is worth being precise about how.
m.fully_readis a private bookmark;m.readis a public receipt. They are distinct object types in the client-server API. Receipts are delivered as ephemeral data (EDUs) and never enter the room's persistent event DAG. The rationale appears in MSC2285: EDUs "have no reference point in the DAG, meaning servers won't agree on whether the EDU is authorised". High-frequency, low-stakes UI state does not justify permanent, federated, cryptographically-ordered history.River has no ephemeral channel. Contract state is the only transport we have. So a River implementation would land in exactly the place Matrix refused to put it, and would be strictly worse along every axis: permanently stored, individually signed, replicated to every subscriber, CRDT-merged on every change, and in a public room readable by anyone holding the room key, including hosting peers who are not members of the room at all. Matrix receipts are at least scoped to member-facing homeservers.
3. The client cost lands on three axes simultaneously
PR #458 (merged 2026-07-23) fixed mobile jank traced to an O(history)
use_memore-running over the whole message list on everyROOMSmutation. Receipts in contract state would re-create that pressure from a different direction:ROOMSon every client and re-running every subscribed memo. The mutation rate would scale with the number of people actively reading, which is the worst possible scaling for the pathology we just fixed.IntersectionObserver, on a bottom sentinel (ui/src/components/conversation.rs:1160-1220). Per-message observers on mobile are a real cost.max_membersdefaults to 200 (common/src/room_state/configuration.rs).Plus the network cost: each read update is a signed CRDT delta broadcast to every subscriber, immediately after the 0.2.105 work on update-broadcast load.
4. Owner-controlled receipts have no real precedent, and could not be enforced here
I could not find a consumer app where a specific group's creator toggles receipts for that group's members. The closest real example is Microsoft Teams, where an IT administrator sets an org-wide messaging policy to "User controlled", "On for everyone" or "Off for everyone", and users cannot override the latter two (Microsoft support). That is tenant-level and server-enforced, which is a different thing from a room owner's preference.
More importantly, River could not enforce such a flag. Room state is public and clients are user-controlled, so a modified client can publish receipts against the owner's setting, or withhold them despite it. The flag would be advisory, which is the weakest possible form for a privacy control: it looks like a guarantee and is not one.
5. The social cost is documented, and the industry response has been to add opt-outs
Survey research on read receipts (Lynden and Rasmussen, Copenhagen, n=108) reports social anxiety, senders over-checking, receivers avoiding opening messages, and roughly a third of respondents reporting feeling ignored. Every platform's response has been to add escape hatches: the Signal, WhatsApp and iMessage toggles, and Matrix's private-receipt MSC. None have added more receipts.
Worth noting that WhatsApp groups always show receipts with no per-member opt-out, and that is the specific case people complain about. Owner-mandated receipts would have the same shape.
The real gap is cross-device, and it should be solved privately
The limitation users actually hit is not that others cannot see their position. It is that they lose their own position when moving between devices.
Delegate secrets live on the node, not the device (
delegates/chat-delegate/, origin-partitioned key/value store). Two devices pointed at the same gateway share the marker; two local nodes do not. So read on your phone against a local node, open your laptop against a different node, and you start over.Matrix hit this exact problem and answered it without publishing anything: MSC2285 added
m.read.private, a receipt that syncs your own unread state across your own devices and never reaches other users.The River equivalent is the mergeable private delegate data direction @sanity floated on 2026-07-05: let delegates optionally expose a merge operation so private data folds together across a user's peers. A last-read pointer is close to an ideal pilot for that:
That is a much gentler first case than secrets with key and DEK reconciliation across replicas.
If we decide to build receipts anyway
Feasibility is not the constraint.
MemberInfoV1(common/src/room_state/member_info.rs) is already exactly the required pattern: each member writes a self-signed entry, one record per member, higher version wins with a signature tiebreak, entries pruned when the member is pruned,BTreeMapsummaries for deterministic anti-entropy. ALastReadV1would mirror it almost line for line. That ease is the risk, not the argument in favour.If we do it, the constraints that follow from the above:
Caveats on the evidence
Related: #459 (the resume-reading feature this came from), #446, #70
[AI-assisted - Claude]