Skip to content

Group direct messages (GRYT-679) - #87

Merged
sivert-io merged 3 commits into
mainfrom
claude/GRYT-679-group-dms
Aug 29, 2026
Merged

Group direct messages (GRYT-679)#87
sivert-io merged 3 commits into
mainfrom
claude/GRYT-679-group-dms

Conversation

@sivert-io

Copy link
Copy Markdown
Member

More than two people in a conversation. Server half.

Most of the shape was already there from #82kind on the conversation, a membership table rather than two columns, a list of other members in the view — so this is largely finishing what was started.

The decision worth your time

Adding somebody to a one-to-one does not convert it. dm:group:create makes a new conversation and the pair conversation stays exactly as it was, history intact.

That's a privacy decision before it's a technical one: what two people said to each other shouldn't become readable by a third because somebody tapped "add". Discord draws the same line. There's a test that holds it — and it also asserts the third person can't chat:fetch the pair history.

If you'd rather it converted, say so — it's a small change here and a large one for anybody who's already used a DM.

The rest

  • Group ids are random; one-to-one ids stay derived. The derived pair-hash is what makes opening a DM idempotent from either end, and it can't survive membership changing — add somebody and the id changes, so the conversation reads as a different one with no history.
  • Anybody in a group may add; anybody may leave; nobody may remove anybody else. That keeps a moderation model out of a conversation that has no moderators, and needs no further decisions later.
  • Leaving is not hiding. Hiding is your own sidebar and a message brings it back (Let somebody hide a direct message (GRYT-678) #86); leaving drops the membership row, so nothing arrives afterwards and the history stops being yours. The last person out purges the conversation — the same sweep server:leave already runs, at the other moment a conversation can empty.
  • other is kept alongside the new members, pointing at the first of them. Redundant on purpose: a client built before groups reads it, and dropping it turns each of those into a crash on other.nickname rather than a client that just doesn't know what a group is.
  • Ten per group. Every message fans out to all of them and the member list rides in every dm:list. The number is Discord's; what matters is that it's decided here rather than by whoever first tries a hundred.

Testing

Nine new cases: three people all see it, the pair conversation survives untouched and stays unreadable to the third, delivery reaches members and nobody else, anyone in it can add, leaving stops delivery and doesn't come back, the two-person floor, an outsider refused, naming reaches everybody, and leaving a one-to-one refused.

347 pass (was 338). Typecheck clean, no new lint warnings.

Not in this PR

Client and mobile UI. Calling is GRYT-680 and lands on top of this.

🤖 Generated with Claude Code

sivert-io and others added 3 commits August 29, 2026 13:28
More than two people in a conversation. Most of the shape was already there
from GRYT-671 — `kind` on the conversation, a membership table rather than
two columns, a list of other members in the view — so this is largely
finishing what was started.

Adding somebody to a one-to-one does not convert it. `dm:group:create` makes
a new conversation and the pair conversation stays exactly as it was, with
its history. That is a privacy decision before it is a technical one: what
two people said to each other should not become readable by a third because
somebody tapped "add". Discord draws the same line. There is a test that
holds it, including that the third person cannot fetch the pair history.

A group's id is random where a one-to-one's is derived from its pair. The
derived id is what makes opening a DM idempotent from either end, and it
cannot survive membership changing — add somebody and the id would change,
so the conversation would read as a different one with no history.

Anybody in a group may add somebody; anybody may leave; nobody may remove
anybody else. That last one keeps a moderation model out of a conversation
that has no moderators, and it is the answer that needs no further decisions
later.

Leaving is not hiding, and the two are kept apart deliberately. Hiding is
your own sidebar and a message brings it back; leaving drops the membership
row, so nothing arrives afterwards and the history stops being yours. The
last person out purges the conversation, which is the same sweep
`server:leave` already runs, at the other moment a conversation can empty.

A group has no name until somebody gives it one. The fallback is built from
who is in it, by the clients rather than here, so it follows a rename
instead of going stale.

The view keeps `other` alongside the new `members`, pointing at the first of
them. It is redundant on purpose: a client built before groups reads it, and
dropping it would turn each of those into a crash rather than a client that
simply does not know what a group is.

Ten people per group. A cap because every message fans out to all of them
and the member list rides in every `dm:list`; the number is Discord's and
what matters is that it is decided here rather than by whoever first tries a
hundred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`dm:group:rename` becomes `dm:group:update`, taking both. One event because
the screen that changes either changes both, and a client sending them
separately would draw a group renamed but not re-pictured for a round trip.

`icon_file_id` holds an upload and nothing else. A group with none is drawn
from its name by the clients, the same way a server with no icon is — storing
a generated image would freeze it against a group that later gets renamed,
and the drawing is cheap enough that there is no reason to keep it.

So `null` on either field means "go back to the drawn one" rather than "has
none": no name reads off the members, and no upload draws from that name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`dm:group:create` accepts `iconFileId`. The client uploads the picture before
the group exists, so without this it would have to wait for `dm:opened` to
learn the id and send a second event — and the group would exist, briefly,
wearing the drawn icon it was not meant to have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sivert-io
sivert-io merged commit 51289d8 into main Aug 29, 2026
1 check passed
@sivert-io
sivert-io deleted the claude/GRYT-679-group-dms branch August 29, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant