Third and final surface with the encrypted-replaceable-list clobber pattern
(after Stash and Contacts). useDismissCrewNotification's cross-device sync path
built the published set from cache-or-local-mirror, which are both empty on a
fresh device before the useCrewDismissals read query resolves. A non-interactive
user dismissing a notification in that window would publish a set with only the
new id, overwriting — and wiping — the previously-synced dismissals from other
devices.
Fix: before encrypting/publishing, re-fetch + decrypt the remote dismissal set
and union it with the pending id (and keep the local mirror + cache aligned).
On read failure it still publishes the local union — dismissals are additive, so
we can only ever under-merge, never silently wipe.
Lower stakes than Stash/Contacts (worst case: a handled invite re-appears in the
bell on another device, not real data loss), but it keeps the fetch-before-write
discipline consistent across every encrypted list in the app. Amber path is
unchanged — still local-only, zero extra prompts.
Bumps to 1.2.25 (Android versionCode 10225 auto-derived).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Accepting/declining a crew invite (and handling join-requests) fired an
encrypted NIP-78 cross-device sync on every dismissal, costing an extra
encrypt + sign Amber approval on top of the required membership signature.
For interactive (Amber) signers, dismissal is now kept local-only:
• Accept invite → 1 Amber prompt (membership attestation only)
• Decline invite → 0 Amber prompts (local dismissal only)
The dismissal still applies instantly and permanently on-device; only the
cross-device "clear it on my other devices too" nicety is dropped for Amber.
Extension/nsec/bunker signers keep the silent encrypted sync (free for them).
Bumps to 1.2.19 (Android versionCode 10219 auto-derived).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
With Amber as signer, every nip44 decrypt is a foreground intent (visible
popup). Several hooks decrypt inside auto-running queries, so Amber flashed on
navigation/focus and (worst case) storms — one popup per encrypted event, and
polling queries re-fired every few seconds.
New helper isInteractiveSigner() (signerCapabilities.ts) detects the Amber
(x-amber) signer, where each crypto op prompts the user. Hooks now gate passive
decryption for interactive signers only; extension/nsec/bunker keep their
silent, live-polling behaviour.
Fixes:
- useCrewDismissals (GLOBAL via header NotificationBell — the main offender):
Amber users now use the instant localStorage mirror only, never passively
decrypt the synced list; disabled refetchOnWindowFocus + Infinity staleTime.
Dismissals still sync OUT on the write path.
- useDirectMessages (all 4 DM query hooks): added amberSafeQueryOptions() —
no background polling and no focus refetch for Amber (these decrypt up to
~200 gift wraps × 2 each); silent signers keep the 60s poll.
- useStash / usePrivateContacts: disabled focus refetch + Infinity staleTime
for Amber so the single decrypt runs once, not on every focus.
- useCordnChat: dropped the 5s live poll for Amber (was a popup every 5s);
silent signers keep it.
Scanned every signEvent call site: all are inside mutations/user callbacks
(zaps, invites, DMs, chat, follows, purchases, NIP-98 auth) — no passive
auto-signing anywhere. Deliberate user actions still prompt Amber as intended.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Fixes the caveat that dismissing a handled invite/join-request on one device
left it un-dismissed on another (it was per-device localStorage).
- New useCrewDismissals hook: stores the dismissed-id set as a single private,
addressable NIP-78 event (kind 30078, d=skatespots:crew-notif-dismissals)
with a NIP-44 payload encrypted to the user's own key. One list covers both
invites and join-requests.
- useDismissCrewNotification: optimistic local update + best-effort encrypted
publish, so dismissals feel instant and sync across every device the user
logs in from.
- Graceful degradation: always resolves the UNION of the synced list and a
local mirror, so it still works offline and on signers without NIP-44 (they
just don't sync) — no regression from the old behavior.
- Rewired useCrewInvites (accept/decline/approve/dismiss) to the synced set;
removed the two localStorage-only dismissal helpers. Filtering moved into
useCrewNotifications so both invite + request lists share one source of truth.
- Documented the kind-30078 dismissal event in NIP.md.
Build clean, no console errors. Not deployed.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>