Audit of the crew Chat tab (see CREW_CHAT_AUDIT.md): - A sent crew message never appeared: useSendDirectMessage invalidated the list keys but not ['group-messages', gid, ...] (the thread key). Now optimistically inserts the sender's own plaintext (no refetch -> no Amber pop-ups). - Every reader decrypted every wrap under its own query key, so each view (Messages, then each crew thread) re-decrypted the whole inbox. Added a shared, coalesced, bounded session decrypt cache (decryptGiftWrap), successes only. - Threads dedup by rumor id; CrewChat guards double-send with a ref; the Cordn path optimistically inserts and invalidates only for silent signers.
3.4 KiB
3.4 KiB
Crew chat — audit (2026-10-13)
Scope: the crew page's Chat tab (CrewChat.tsx) across both transports — the
built-in NIP-17 group chat and the Cordn sealed-relay path (useCordnChat)
— plus the shared DM readers it depends on (useDirectMessages.ts).
Findings
| # | Sev | Status | Finding |
|---|---|---|---|
| 1 | High | Fixed | A sent crew message didn't appear. useSendDirectMessage invalidated direct-messages / conversations / group-conversations, but the crew thread reads ['group-messages', gid, …] — never invalidated. For a silent signer it showed up on the next 60 s poll; for Amber (no polling, session-cached) it never did. Fixed with an optimistic cache insert of the sender's own plaintext (no refetch → no extra signer pop-ups). |
| 2 | High | Fixed (1.6.4) | Amber "didn't respond" on send. The per-recipient gift wraps (and the sealed-relay roster seal) were created with Promise.all — concurrent nostrsigner: intents collide, one is answered and the rest hit the 60 s native timeout. Now sequential for an interactive signer. |
| 3 | High | Fixed | Every chat view re-decrypted the whole inbox. Each reader (useConversations, useConversation, useGroupConversations, useGroupConversation) decrypted every wrap — 2 signer ops each — and each has its own query key, so opening Messages decrypted everything, then opening one crew thread decrypted it all again, and again per thread. Added a shared, coalesced, bounded session decrypt cache (decryptGiftWrap) keyed viewerPk:wrapId; only successes are cached, so a transient failure retries. |
| 4 | Med | Fixed | No dedup by rumor id in a thread — a re-sent rumor (same id, two wraps) could render twice. Threads now dedup by id before sorting. |
| 5 | Med | Fixed | Double-send: pressing Enter twice before React re-rendered isPending sent twice. Added an in-flight ref guard. |
| 6 | Med | Fixed | The Cordn path invalidated its query on every send → an Amber refetch storm. Now optimistically inserts the sent message and only invalidates for silent signers. |
| 7 | Med | Open | Transport switches silently. A crew with a coordinator tag uses the sealed-relay Cordn path; without one it uses NIP-17. They have separate histories, so a crew that later gains a coordinator appears to "lose" its chat. New crews now get the SkateSpots coordinator by default, so most new crews take the Cordn path. Needs a product decision: label it clearly, or standardise on one. |
| 8 | Med | Open | Read limit 200. Threads fetch the N most recent wraps addressed to the viewer and filter client-side (the wrap hides its participants, so the relay can't filter). A busy inbox can push older crew messages out of the window → the chat looks empty. Could raise the limit for group threads. |
| 9 | Low | Open | No images/attachments in the crew Chat (text only). The MLS Private chat got images; the NIP-17 path didn't. |
| 10 | Info | — | With the cache, an Amber read is now one decrypt per wrap per session instead of one per view. Sends still cost 2 approvals per member (NIP-17) vs ~1 per message on the MLS path. |
Net effect
- Sending now shows your own message immediately.
- Opening Messages ⇄ a crew thread ⇄ another crew thread no longer re-prompts.
- Amber stops timing out mid-send.
Not click-tested in a real browser this session; npm test + check-i18n green.