Repository navigation
fix(web): show a rejected send as Not sent on its tile, with Retry and Delete - #48
Conversation
…d Delete Read the SDK's local echo status instead of treating a resolved composer submit as delivered. NOT_SENT tiles are muted, drop the timestamp, and carry the reason, Retry (client.resendEvent, same txnId) and Delete (client.cancelPendingEvent). useTimeline re-snapshots on LocalEchoUpdated. The composer no longer prints the raw MatrixError for message content; custom events (dev.zooid.*) have no tile, so they keep the composer error and drop their echo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ogether Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
zooidbot
left a comment
There was a problem hiding this comment.
Architect review — this matches the brief and the #47 checklist. No blockers.
I read the diff at 7ef0b04 against the issue's acceptance list. Rejected sends now live on the SDK's own NOT_SENT echo, useTimeline repaints on LocalEchoUpdated and also checks echoKey (which is needed, since the echo changes in place), and Retry calls resendEvent on the same event, so the txnId is reused. The integration test covers the 403 case, Retry, Delete, the repaint back to sending, and /clear.
Rulings on the deviations
- (a) "not a member" only for membership 403s: accepted. The exact error in #47 ("Auth check failed: sender's membership 'invite' is not 'join'") matches
/membership/, so the dogfood case gets the right text. A power-level 403 saying "not a member" would have been wrong, and the brief's example was too loose. - (b)
dev.zooid.*failures keep the composer error and cancel their echo: accepted. A failed/clearthat left a "new session" divider would recreate this same bug for slash commands. Showing the error in the composer and dropping the echo is the honest choice.
Non-blocking (follow-up is fine, no need to hold this PR)
- Failed media tile still shows its timestamp.
event-tile.tsxwrapsMediaMessagewithopacity-60and the failure line, butMediaMessagestill renders its normal timestamp. That partly misses checklist item 1 ("no avatar/timestamp as if it landed"). It's a small gap, since the "Not sent" line is right there, but it's worth a follow-up issue. - The stories don't render the real tile.
send-state.stories.tsxbuilds its ownRowaroundMessageTileinstead of renderingTextMessagewith aNOT_SENTevent, so the story can drift from what users see (e.g. the hidden actions and theopacity-60class are copied, not shared). Fine for now. If the tile changes again, switch the story to the real component. EventSendFailure's Retry swallows the rejection. That's fine, because the SDK sets the echo back toNOT_SENTwith the new error and the tile repaints. Noting it so nobody "fixes" it into a toast.
Checked: the client doesn't set threadSupport, so a thread reply's echo sits in the room's unfiltered timeline set and notSentEchoes finds it. Failed thread replies get the tile too, not the composer line.
Not verified by me: I haven't run the stories or done a live smoke. Ori's smoke steps are in the task thread. Light theme for failed-not-joined/in-timeline is still unchecked, according to the PR body.
Closes #47
A message the server rejects (e.g. sending from an invite-only room you've only been invited to) now shows as Not sent on its own tile, with the reason, Retry (reuses the same txnId via
client.resendEvent) and Delete (client.cancelPendingEvent). The rawMatrixErrorline under the composer is gone for message sends.What changed (
packages/web)use-timeline.ts: re-snapshot onRoomEvent.LocalEchoUpdated; the cached snapshot also compares echo status, since an echo mutates in place.NOT_SENTis muted, shows no timestamp, and renders "Not sent · " with Retry/Delete.SENDING/QUEUED/ENCRYPTINGshows "Sending…" in place of the timestamp. UI is in the newsend-state.tsx.m.room.message(including media) is swallowed and the composer clears. Upload and staging errors still show in the composer..github/workflows/*untouched.Deviations from the brief
dev.zooid.*sends (e.g./clear) have no failure tile, so they keep the composer error and their local echo is cancelled. Otherwise a failed/clearwould leave a "new session" divider in the timeline as if delivered.Tests:
pnpm testinpackages/web— 152 files, 960 tests pass;pnpm typecheckclean (package has no lint script). Newcomposer.failed-send.integration.test.tsxcovers the 403 case (Not sent + Retry, no raw error under the composer), Retry passing the same event/txnId toresendEvent, Delete, repaint when the echo returns to sending, and the slash-command case.UX Tester (stories
Timeline/Send state, two rounds), after fixing round 1:failed-not-joineddark, and at 375×800 Retry/Delete stay togetherin-timelinedark: failed row visibly more muted than the delivered rowfailed-other-errorlightsendinglight (round 1)opacity-60is unchanged.Not verified:
failed-not-joinedandin-timeline; console status in round 2.run-story-tests) and/verify-web: Storybook doesn't run in my sandbox and UX Tester couldn't run them.invite, then retry after joining): needs the full stack, which the sandbox can't reach.Knowledge impact: ZNC001 composer/timeline behaviour (a failed send now shows a Not sent state instead of looking delivered); no spec text edited.
Plan feedback:
git grep EventStatuswas empty and the SDK keeps the echo. That held.getPendingEvents()throws because the client uses the default Chronological ordering, so detection goes through the timeline; anduseTimeline's snapshot cache hides in-place status changes, so listening forLocalEchoUpdatedalone isn't enough.EventTile's media branch have no tile that can show a failure, which forced deviation (b)./clearonly parses when the composer is thread-scoped, which cost a test round.🤖 Generated with Claude Code