An event is not a message, and a notice is not either (GRYT-896) - #374
Merged
Conversation
Two changes, both about the same mistake: everything a server said became a
chat message shaped like somebody talking.
**Events become one quiet row.**
A join used to fill a whole message slot — the word "System" where a nickname
goes, in the same weight and size, a 51px avatar column held empty so the
absence read as a missing avatar rather than a different kind of thing, a
timestamp on its own line, and reactions and a hover toolbar, so you could react
to somebody joining. Two people arriving cost two full rows.
The avatar column carries a small arrow now — in for joined, out for left — so
the space is doing something and the row is unmistakably not somebody talking.
No name line: there is no author. The time trails the text.
The left edge every message shares stays where it is. A centred rule would say
"not a message" more loudly and break that edge, and a channel where people come
and go is exactly where a run of rules chops the conversation into fragments.
Which arrow is read off the text rather than a new column, because the server
sends these as ordinary message rows and two verbs are not worth a migration.
Anything unrecognised draws the "in" arrow, which is the right guess for
anything that is not a departure.
**A directed notice is not in chat at all.**
The server sends `{ kind, version }` and this side owns every word. That is the
security property: a panel in app furniture, carrying text a server chose,
addressed to one person, is a phishing message with a nice border. Taking the
text away is the only version of this that cannot be turned into one.
`parseServerNotice` re-checks the shape rather than trusting it. The server
validating its own output guards against a bug in the server; this guards
against the server, which is somebody else's machine and need not be running our
code. Unknown kinds, malformed values and smuggled extra fields are all dropped
in silence — a malformed notice is a server bug or a server trying something,
and neither is worth interrupting somebody with.
The panel sits below the channel header and above the messages, inside the
server's area rather than in the app's own chrome, and says "From <server>, only
to you" in the frame. Anything painted in Gryt's chrome should only ever be
Gryt. Its two links are constants in this file; a server cannot reach them.
Dismissal is per device and permanent, and the server is not told — a server
learning which of its notices somebody has silenced is the beginning of working
around it.
`check-server-notice.mjs` holds the line, and is wired into CI. Five mutations,
each caught: widening the version pattern, spreading the payload through the
parse, dropping the switch's default arm, reading a URL off the notice, and
pointing the panel's own link at a host we do not own. The last two are source
checks — a panel that interpolated a notice field into an href would typecheck
and look reasonable in review.
One thing the rig caught that nothing else would: `chatView` became a fragment
with two children, and both places that render it drop it into a flex row — so
the chat stopped being the thing that grows and collapsed to a column one
character wide. It is a filling flex column now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sivert-io
force-pushed
the
claude/GRYT-896-directed-notices
branch
from
September 3, 2026 21:50
da5211c to
37028c3
Compare
… (GRYT-896) Two things the design said and the first pass did not do. Both were found by looking at it rather than by any check. **It was rendering above the channel header.** `chatView` put the panel beside `ChatView` rather than inside it, so the panel sat above "# general" — in the app's own chrome, which is the one place the whole argument says it must not be. A server's words in Gryt's furniture is exactly what attributing it was meant to prevent, so the placement was undoing the reason for the attribution. `ChatView` takes an `underHeader` slot now and renders it directly beneath the channel name, above the messages. A slot rather than the panel itself, because `ChatView` does not know about servers — it is the same component for a DM. That also puts `chatView` back to a single element, so the flex column added a few commits ago to stop the chat collapsing is no longer needed. **It was grey.** `--gryt-warning-3` for the ground and `--gryt-warning-11` for the rule and the icon, which is what the design used. On `neutral-3` it read as another panel, and the point of this one is that it is the only thing on the screen asking for something. The fallback in `var(--gryt-warning-11, var(--gryt-danger-11))` is gone too: `@gryt/ui` ships a full warning ramp, so the fallback was dead code that would have quietly turned the notice red if the token had ever been renamed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Client half. Server half is Gryt-chat/server#130 — merge that one first, or this listens for an event nothing sends.
Two changes, one mistake behind both: everything a server said became a chat message shaped like somebody talking.
Events become one quiet row
A join filled a whole message slot — the word "System" where a nickname goes, in the same weight and size, a 51px avatar column held empty so the absence read as a missing avatar rather than a different kind of thing, a timestamp on its own line, plus reactions and a hover toolbar. You could react to somebody joining. Two people arriving cost two full rows.
Now: the avatar column carries a small arrow — in for joined, out for left — so the space is doing something and the row is unmistakably not somebody talking. No name line, because there is no author. The time trails the text. One 34px line.
The left edge every message shares stays where it is. A centred rule would say "not a message" more loudly and break that edge, and a channel where people come and go is exactly where a run of rules chops the conversation into fragments.
Which arrow is read off the text rather than a new column — the server sends these as ordinary message rows and two verbs are not worth a migration. Anything unrecognised draws the "in" arrow, the right guess for anything that is not a departure.
A directed notice is not in chat at all
The server sends
{ kind, version }and this side owns every word. That is the security property: text a server chose, in app furniture, addressed to one person, is a phishing message. Taking the text away is the only version that cannot be turned into one.parseServerNoticere-checks the shape rather than trusting it. The server validating its own output guards against a bug in the server; this guards against the server, which is somebody else's machine and need not be running our code. Unknown kinds, malformed values and smuggled extra fields are dropped in silence — a malformed notice is a server bug or a server trying something, and neither is worth interrupting somebody with.The panel sits below the channel header, above the messages, and says "From <server>, only to you" in the frame. Inside the server's area, never in Gryt's own chrome — anything painted there should only ever be Gryt. Its two links are constants in the file; a server cannot reach them.
Dismissal is per device and permanent, and the server is not told. A server learning which of its notices somebody has silenced is the beginning of working around it.
Tests
check-server-notice.mjs, wired into CI. Five mutations, each caught:default: return nullremovedThe last two are source checks. A panel that interpolated a notice field into an
hrefwould typecheck and look reasonable in review.Verified against a running server
Real emit, real listener, real render:
@name joined the server 23:41on one line, no "System" anywhereTwo things the rig caught that nothing else would
chatViewbecame a fragment, and both places that render it drop it into a flex row — so the chat stopped being the thing that grows and collapsed to a column one character wide. It is a filling flex column now.MarkdownRendereremits a block. A second line for four words is the thing this treatment exists to stop. Baseline-aligned flex row instead.tsc -bandeslintclean.🤖 Generated with Claude Code