Spaces:
Sleeping
Sleeping
File size: 4,232 Bytes
71c1c9d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
import * as React from "react";
import type { UpdateItem, ReactionInfo } from "../lib/types";
export type NearbyQueueOptions = {
limit?: number; // max items to show in one run (default 5)
storageNamespace?: string; // localStorage key prefix
};
function readSeenSet(key: string): Set<string> {
try {
const raw = localStorage.getItem(key);
if (!raw) return new Set();
const arr = JSON.parse(raw);
return new Set(Array.isArray(arr) ? arr : []);
} catch {
return new Set();
}
}
function writeSeenSet(key: string, set: Set<string>) {
try {
localStorage.setItem(key, JSON.stringify(Array.from(set)));
} catch {
// ignore quota errors
}
}
export function useNearbyQueue(
nearby: UpdateItem[],
reactionsById: Record<string, ReactionInfo>,
sessionId: string,
onReact: (rid: string, action: "verify" | "clear") => void | Promise<void>,
opts: NearbyQueueOptions = {}
) {
const { limit = 5, storageNamespace = "pm_seen_v1" } = opts;
const storageKey = React.useMemo(
() => `${storageNamespace}:${sessionId || "anon"}`,
[storageNamespace, sessionId]
);
// persistent set of rids shown to the user in this session
const seenRef = React.useRef<Set<string>>(readSeenSet(storageKey));
React.useEffect(() => {
// if sessionId changes, reload the seen set
seenRef.current = readSeenSet(storageKey);
}, [storageKey]);
const [queue, setQueue] = React.useState<UpdateItem[]>([]);
const [index, setIndex] = React.useState(0);
const [open, setOpen] = React.useState(false);
const [leaving, setLeaving] = React.useState(false);
// build a fresh queue whenever inputs change
React.useEffect(() => {
const out: UpdateItem[] = [];
for (const u of nearby) {
if (!u || u.kind !== "report" || !u.rid) continue;
const r = reactionsById[u.rid];
const already = !!(r?.me?.verified || r?.me?.cleared);
if (already) continue; // don't nag if already handled this session
if (seenRef.current.has(u.rid)) continue; // don't re-show in this session
out.push(u);
if (out.length >= limit) break;
}
setQueue(out);
setIndex(0);
setLeaving(false);
}, [nearby, reactionsById, limit]);
const current = queue[index] || null;
const total = queue.length;
const markSeen = React.useCallback(
(rid?: string | null) => {
if (!rid) return;
if (!seenRef.current.has(rid)) {
seenRef.current.add(rid);
writeSeenSet(storageKey, seenRef.current);
}
},
[storageKey]
);
const advance = React.useCallback(() => {
setIndex((i) => {
const next = i + 1;
return next < total ? next : i;
});
}, [total]);
const openQueue = React.useCallback(() => {
if (total > 0) setOpen(true);
}, [total]);
const closeQueue = React.useCallback(() => {
// fully dismiss: stop animations, clear queue, reset index
setOpen(false);
setLeaving(false);
setQueue([]);
setIndex(0);
}, []);
// unified action handler with small exit animation
async function act(action: "verify" | "clear" | "skip") {
if (!current) return;
const rid = current.rid!;
setLeaving(true);
if (action === "verify" || action === "clear") {
try {
await onReact(rid, action);
} catch {
// ignore; reconcile via hydration later
}
}
// mark as seen so we won't show it again
markSeen(rid);
// wait for the CSS transition (keep in sync with component styles)
await new Promise((r) => setTimeout(r, 220));
// move to next or close
if (index + 1 < total) {
setLeaving(false);
setIndex((i) => i + 1);
} else {
// last one: clear queue so it won't auto-reopen
setQueue([]);
setIndex(0);
closeQueue();
}
}
const verify = React.useCallback(() => act("verify"), [current, act]);
const clear = React.useCallback(() => act("clear"), [current, act]);
const skip = React.useCallback(() => act("skip"), [current, act]);
return {
// state
open,
leaving,
current,
index,
total,
queue,
// actions
openQueue,
closeQueue,
verify,
clear,
skip,
// helpers
markSeen,
} as const;
}
|