SkateSpots/CREW_CHAT_AUDIT.md
SkateSpots 8e7c5bfa09 fix(crew-chat): show sent messages, stop re-decrypting the inbox
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.
2026-10-13 00:21:42 +07:00

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.