1"use client";
2
3
30import { useEffect, useMemo } from "react";
31import { authStore } from "@/lib/auth-store";
32import { channelForPage, realtime } from "@/lib/realtime";
33import { stripRoomKey } from "./realtime-frame-handler";
34import { PresenceEmitter } from "@/lib/presence-emitter";
35import { useBlockPatchStore } from "@/stores/block-patch-store";
36import { usePresenceCaretStore } from "@/stores/presence-store";
37import type { Presence, RealtimeServerEvent } from "@/lib/types";
38
39const EMPTY_PRESENCE: Record<string, Presence> = {};
40
41
43function usePresenceEmitter(active: boolean): void {
44 useEffect(() => {
45 if (!active) return;
46 const emitter = new PresenceEmitter({ send: (s) => realtime.sendPresence(s) });
47 const offCaret = usePresenceCaretStore.subscribe((state) => emitter.setCaret(state.caret));
48 emitter.setCaret(usePresenceCaretStore.getState().caret);
49 emitter.setViewing(
50 typeof document === "undefined" ? true : document.visibilityState === "visible",
51 );
52 const onVisibility = (): void => {
53 emitter.setViewing(document.visibilityState === "visible");
54 };
55 document.addEventListener("visibilitychange", onVisibility);
56
57
58
59
60 const offStatus = realtime.onStatus((status) => {
61 if (status === "connected") emitter.assert();
62 });
63 return () => {
64 document.removeEventListener("visibilitychange", onVisibility);
65 offCaret();
66 offStatus();
67 emitter.dispose();
68 usePresenceCaretStore.getState().setCaret(null);
69 realtime.sendPresenceLeave();
70 };
71 }, [active]);
72}
73
74
76export function usePresence(pageId: string | null): void {
77 usePresenceEmitter(pageId !== null);
78}
79
80
83export function usePagePresence(pageId: string | null): Presence[] {
84 const presenceMap = useBlockPatchStore((s) =>
85 pageId ? (s.presence[pageId] ?? EMPTY_PRESENCE) : EMPTY_PRESENCE,
86 );
87 return useMemo(() => Object.values(presenceMap), [presenceMap]);
88}
89
90export interface DatabasePresence {
91 presence: Presence[];
92}
93
94
101export function useDatabasePresence(databaseId: string | null): DatabasePresence {
102 usePresenceEmitter(databaseId !== null);
103
104 useEffect(() => {
105 if (!databaseId) return;
106
107
108 realtime.connect(() => authStore.accessToken);
109 const channel = `database:${databaseId}`;
110 realtime.subscribe(channel);
111 const store = useBlockPatchStore.getState();
112 const onFrame = (event: RealtimeServerEvent): void => {
113 if (event.type === "presence") {
114 store.upsertPresence(stripRoomKey(event.pageId), {
115 userId: event.user.id,
116 name: event.user.name,
117 color: event.user.color,
118 cursor: event.cursor ?? null,
119 viewing: event.viewing,
120 lastSeen: event.lastSeen,
121 });
122 } else if (event.type === "presence-leave") {
123 store.removePresence(stripRoomKey(event.pageId), event.userId);
124 }
125 };
126 const offs = [realtime.on("presence", onFrame), realtime.on("presence-leave", onFrame)];
127 return () => {
128 for (const off of offs) off();
129 realtime.unsubscribe(channel);
130
131 useBlockPatchStore.getState().reset(databaseId);
132 };
133 }, [databaseId]);
134
135 const presenceMap = useBlockPatchStore((s) =>
136 databaseId ? (s.presence[databaseId] ?? EMPTY_PRESENCE) : EMPTY_PRESENCE,
137 );
138 return useMemo(() => ({ presence: Object.values(presenceMap) }), [presenceMap]);
139}
140
141
142export { channelForPage };
143