You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 where sphere exists but useIdentity has no data yet, so that bucket can hold live DM data.
Silent draft loss (DM only). The [addressId] effect at useChat.ts:90-96 calls setMessageInput('') unconditionally, so a half-typed message died whenever the key flipped. useGroupChat.ts:113-120 deliberately omits setMessageInput, 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 useIdentityplaceholderData, 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
isGroupChatConnected is never reset on any loss of Sphere.ServicesProvider.tsx:7 starts false, :10 reads sphere?.groupChat ?? null, and the effect at :13-14 early-returns, so neither :30 nor :38 runs. GroupChatModule.destroy() (sphere-sdk modules/groupchat/GroupChatModule.ts:215-241) emits no groupchat:connection {connected:false} — the only false emitter is connect()'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:236 throws 'No conversation selected' when the real cause is !sphere, and useChat.ts:330 → DMChatSection.tsx:121-125 calls mutateAsync un-awaited with no .catch(), so the rejection is unhandled AND toasted AND sent to Sentry. This is the DM half of [HIGH] Group message send failure silently discards the typed message #454, which fixed exactly this shape for the group composer via requireSent.ts.
MiniChatBubbles vanishes entirely.MiniChatBubbles.tsx:98-100 returns null when conversations.length === 0, unmounting the whole portal — launcher, unread badge, every open window — while locked, and miniChatStore.ts keeps the window ids so they pop back on unlock. It is mounted on every dashboard route (DashboardLayout.tsx:67), so it sits outside DesktopLayout and is NOT covered by the locked-surface gate.
Unread badges silently read 0.useDmUnreadCount.ts:11-16 and useGroupUnreadCount.ts:12-17 fall back to data = 0, so TabBar.tsx:96-105, Sidebar.tsx:56-57 and DesktopShortcuts.tsx:155-159 assert "nothing new" when the truth is "not listening". Display-only: no message is lost, because NostrTransportProvider.ts:1223-1234 advances the DM cursor only after a successful unwrap.
Marketplace review affordances unmount instead of explaining.ProjectReviewsSection.tsx:159/:220/:273/:282 and ReviewReplies.tsx:27/:86/:96/:119 gate on !!sphere, so a locked wallet reads as "your account is not eligible to review". ProjectPage is behind no blocker.
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:71anduseGroupChat.ts:102both deriveaddressIdasdirectAddress ? buildAddressId(directAddress) : 'default'.directAddresscomes from theuseIdentityquery, so any loss of Sphere — a lock, a logout, a slow first render — flips the key to the shared literal'default'.Two consequences:
'default'bucket. There is a real window wheresphereexists butuseIdentityhas no data yet, so that bucket can hold live DM data.[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 — giveuseIdentityplaceholderData, or hold the last known address — which touchesuseIdentity/useSphereEvents/useChat/useGroupChatand 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 [HIGH] Group message send failure silently discards the typed message #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.