File size: 2,126 Bytes
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"use client";

import { useEffect, useMemo, useState } from "react";
import { useStudentActivityVersion } from "@/hooks/useStudentActivity";
import {
  getSyncedProgress,
  syncProgressEvents,
} from "@/lib/api/progress-sync";
import { learnerScopeId } from "@/lib/phase3-learning";
import {
  listActivities,
  mergeActivityEvents,
  type ActivityEvent,
} from "@/lib/student-activity";

export type ActivitySyncState = "device" | "syncing" | "synced" | "error";

/**
 * One account-aware activity ledger for Home and Progress. Offline events stay
 * visible immediately, then upload idempotently and merge with activity from
 * the student's other devices.
 */
export function useSyncedStudentActivity(limit = 50) {
  const version = useStudentActivityVersion();
  const scopeId = learnerScopeId();
  const localEvents = useMemo(() => {
    void version;
    return listActivities(Math.min(limit, 100));
  }, [limit, version]);
  const [remoteEvents, setRemoteEvents] = useState<ActivityEvent[]>([]);
  const [syncState, setSyncState] = useState<ActivitySyncState>(() =>
    scopeId ? "syncing" : "device",
  );

  useEffect(() => {
    let cancelled = false;

    async function refresh() {
      // Defer state changes so the effect never performs a synchronous state
      // write during React's commit phase.
      await Promise.resolve();
      if (cancelled) return;
      if (!scopeId) {
        setRemoteEvents([]);
        setSyncState("device");
        return;
      }

      setSyncState("syncing");
      try {
        if (localEvents.length) await syncProgressEvents(localEvents);
        const remote = await getSyncedProgress();
        if (cancelled) return;
        setRemoteEvents(remote.recent_events);
        setSyncState("synced");
      } catch {
        if (!cancelled) setSyncState("error");
      }
    }

    void refresh();
    return () => {
      cancelled = true;
    };
  }, [localEvents, scopeId]);

  const events = useMemo(
    () => mergeActivityEvents(remoteEvents, localEvents).slice(0, limit),
    [limit, localEvents, remoteEvents],
  );

  return { events, syncState, version };
}