Chat caches keyed on a nullable identity, and five surfaces that misreport a missing Sphere
Found while auditing every wallet surface for its behaviour under a lock. All of these are byte-identical to main — none was introduced by the graceful-lock branch, and none is reachable by a user now that locked chat surfaces are gated. They are grouped here because they share one root cause and one blast radius.
Root: chat caches are keyed on an identity that can vanish
useChat.ts:71 and useGroupChat.ts:102 both derive addressId as directAddress ? buildAddressId(directAddress) : 'default'. directAddress comes from the useIdentity query, so any loss of Sphere — a lock, a logout, a slow first render — flips the key to the shared literal 'default'.
Two consequences:
- Cache aliasing. Every wallet that ever hits the fallback shares one
'default'bucket. There is a real window wheresphereexists butuseIdentityhas no data yet, so that bucket can hold live DM data. - Silent draft loss (DM only). The
[addressId]effect atuseChat.ts:90-96callssetMessageInput('')unconditionally, so a half-typed message died whenever the key flipped.useGroupChat.ts:113-120deliberately omitssetMessageInput, so group drafts survived — the two halves disagreed.
The draft half is already mitigated on the graceful-lock branch: both composers moved to a module-scoped draft store (src/components/chat/draftStore.ts) so an unmount or a key flip no longer destroys typed text. The aliasing is not fixed. The real fix is to stop keying caches on a nullable identity — give useIdentity placeholderData, or hold the last known address — which touches useIdentity / useSphereEvents / useChat / useGroupChat and carries live-address-switch blast radius, so it wants its own PR.
Related, same area
isGroupChatConnectedis never reset on any loss of Sphere.ServicesProvider.tsx:7starts false,:10readssphere?.groupChat ?? null, and the effect at:13-14early-returns, so neither:30nor:38runs.GroupChatModule.destroy()(sphere-sdkmodules/groupchat/GroupChatModule.ts:215-241) emits nogroupchat:connection {connected:false}— the only false emitter isconnect()'s catch. The flag therefore misreports on any sphere loss, and the two locked states are wrong in opposite directions: a cold start sticks false (forever "Connecting..."), a warm lock sticks true ("Select a group to start" over an empty list). A naive "reset it on lock" turns every warm lock into the forever-spinner, so this needs the SDK to emit on destroy. Has an SDK sub-task.DM send error path.
useChat.ts:236throws'No conversation selected'when the real cause is!sphere, anduseChat.ts:330→DMChatSection.tsx:121-125callsmutateAsyncun-awaited with no.catch(), so the rejection is unhandled AND toasted AND sent to Sentry. This is the DM half of #454, which fixed exactly this shape for the group composer viarequireSent.ts.MiniChatBubblesvanishes entirely.MiniChatBubbles.tsx:98-100returnsnullwhenconversations.length === 0, unmounting the whole portal — launcher, unread badge, every open window — while locked, andminiChatStore.tskeeps the window ids so they pop back on unlock. It is mounted on every dashboard route (DashboardLayout.tsx:67), so it sits outsideDesktopLayoutand is NOT covered by the locked-surface gate.Unread badges silently read 0.
useDmUnreadCount.ts:11-16anduseGroupUnreadCount.ts:12-17fall back todata = 0, soTabBar.tsx:96-105,Sidebar.tsx:56-57andDesktopShortcuts.tsx:155-159assert "nothing new" when the truth is "not listening". Display-only: no message is lost, becauseNostrTransportProvider.ts:1223-1234advances the DM cursor only after a successful unwrap.Marketplace review affordances unmount instead of explaining.
ProjectReviewsSection.tsx:159/:220/:273/:282andReviewReplies.tsx:27/:86/:96/:119gate on!!sphere, so a locked wallet reads as "your account is not eligible to review".ProjectPageis behind no blocker.
Source: unicity-sphere/sphere