Crew dismissals: merge remote set before sync (close clobber pattern)
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>
This commit is contained in:
parent
a31583079b
commit
8affd02bcf
2 changed files with 35 additions and 2 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.2.24",
|
||||
"version": "1.2.25",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.com",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -175,7 +175,40 @@ export function useDismissCrewNotification() {
|
|||
if (!user?.pubkey || !nip44) return; // no NIP-44: local-only, no throw
|
||||
|
||||
try {
|
||||
const payload = JSON.stringify({ ids: [...next] });
|
||||
// Data-safety: this list is a single replaceable event whose encrypted
|
||||
// content is the WHOLE dismissal set, so we rewrite all of it. If the
|
||||
// read query hasn't resolved yet (fresh device: cache empty, local
|
||||
// mirror empty), `next` would be just this one id and we'd overwrite —
|
||||
// and thus WIPE — the previously-synced dismissals from other devices.
|
||||
// So merge in the freshest remote set before publishing. (Low-stakes
|
||||
// data — worst case a handled invite re-appears — but we keep the
|
||||
// fetch-before-write pattern consistent with Stash/Contacts.)
|
||||
const merged = new Set(next);
|
||||
try {
|
||||
const events = await nostr.query(
|
||||
[{ kinds: [DISMISS_KIND], authors: [user.pubkey], '#d': [DISMISS_D], limit: 1 }],
|
||||
{ signal: AbortSignal.timeout(6000) },
|
||||
);
|
||||
const remote = events[0];
|
||||
if (remote?.content) {
|
||||
const decrypted = await nip44.decrypt(user.pubkey, remote.content);
|
||||
const parsed = JSON.parse(decrypted) as { ids?: unknown };
|
||||
if (Array.isArray(parsed.ids)) {
|
||||
for (const rid of parsed.ids) {
|
||||
if (typeof rid === 'string') merged.add(rid);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Couldn't read the remote set — publish the local union anyway; a
|
||||
// dismissal is additive, so at worst we under-merge, never wipe silently.
|
||||
}
|
||||
|
||||
// Keep local mirror + cache aligned with what we're about to publish.
|
||||
writeLocal([...merged]);
|
||||
queryClient.setQueryData(key, merged);
|
||||
|
||||
const payload = JSON.stringify({ ids: [...merged] });
|
||||
const content = await nip44.encrypt(user.pubkey, payload);
|
||||
const event = await user.signer.signEvent({
|
||||
kind: DISMISS_KIND,
|
||||
|
|
|
|||
Loading…
Reference in a new issue