SkateSpots/src/lib/localModeration.ts
shakespeare.diy 79e8376f58 Make admin delete non-blocking + polish notification bell
DELETE HANG (real fix):
The admin takedown required TWO sequential Amber approvals (kind:5
deletion + kind:1985 hide label) and awaited both. On mobile the second
Amber prompt often doesn't resurface, so the button sat on "Deleting…"
until the 90s signer timeout. Now:
- Only the kind:5 deletion is awaited (the required action).
- The target is recorded in a local optimistic-hide store immediately, so
  the app hides it on this device the instant kind:5 succeeds — no waiting
  on a second signature.
- The cross-device kind:1985 label is fired in the BACKGROUND (not
  awaited); a second approval, if needed, happens after the dialog closes.
- The whole mutation is wrapped in a 40s hard timeout so the spinner can
  never hang indefinitely, whatever the signer/relay does.
- useAdminModeration merges the local optimistic set so hides are instant
  and survive reload.

NOTIFICATION BELL UI:
- Brand-lime (#ccff00) glowing badge with ring, replacing the flat teal dot.
- Bell wiggles + soft glow pulse when there are unread items (new `wiggle`
  keyframe).
- Gradient header with an icon chip, inline count, and a pill "Mark read"
  button / "All clear" state.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 22:59:16 +07:00

61 lines
2.1 KiB
TypeScript

/**
* Local (optimistic) admin-moderation store.
*
* WHY THIS EXISTS
* ---------------
* An admin takedown publishes a kind:1985 `hidden` label so the content is
* hidden across devices (see useAdminModeration). But publishing that label
* requires a SECOND Amber approval, which on mobile can be slow or require a
* second app switch. We must NOT make the delete UI wait on it.
*
* So the moment the required kind:5 deletion succeeds, we record the target in
* this local store. useAdminModeration merges it, so the content disappears
* from the app immediately on THIS device. The kind:1985 label still publishes
* in the background for cross-device / other-user persistence.
*
* Persisted to localStorage so an optimistic hide survives a reload even if the
* background label publish hasn't propagated yet.
*/
const STORAGE_KEY = 'skatespots_local_admin_hidden_v1';
interface LocalHidden {
coords: string[];
ids: string[];
}
function read(): LocalHidden {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { coords: [], ids: [] };
const parsed = JSON.parse(raw) as Partial<LocalHidden>;
return {
coords: Array.isArray(parsed.coords) ? parsed.coords : [],
ids: Array.isArray(parsed.ids) ? parsed.ids : [],
};
} catch {
return { coords: [], ids: [] };
}
}
function write(data: LocalHidden): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch {
/* storage full / unavailable — optimistic hide is best-effort */
}
}
/** Record a coordinate + event id as locally hidden by the admin. */
export function addLocalHidden(opts: { coord?: string; id?: string }): void {
const data = read();
if (opts.coord && !data.coords.includes(opts.coord)) data.coords.push(opts.coord);
if (opts.id && !data.ids.includes(opts.id)) data.ids.push(opts.id);
write(data);
}
/** Read the current local hidden sets. */
export function getLocalHidden(): { coords: Set<string>; ids: Set<string> } {
const data = read();
return { coords: new Set(data.coords), ids: new Set(data.ids) };
}