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>
61 lines
2.1 KiB
TypeScript
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) };
|
|
}
|