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;
}