| import { useEffect, useRef, useState } from "react"; |
| import type { Editor as TiptapEditor } from "@tiptap/core"; |
| import type { HocuspocusProvider } from "@hocuspocus/provider"; |
| import { Cloud, CloudOff, Loader2 } from "lucide-react"; |
| import { Tooltip } from "./Tooltip"; |
|
|
| type SyncStatus = "connected" | "syncing" | "saved" | "disconnected"; |
|
|
| interface Props { |
| editorInstance: TiptapEditor | null; |
| providerRef: { current: HocuspocusProvider | null }; |
| } |
|
|
| |
| |
| |
| |
| |
| export function SyncIndicator({ editorInstance, providerRef }: Props) { |
| const [status, setStatus] = useState<SyncStatus>("disconnected"); |
| const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
|
|
| useEffect(() => { |
| const provider = providerRef.current; |
| if (!provider) return; |
| const onConnect = () => setStatus("saved"); |
| const onDisconnect = () => setStatus("disconnected"); |
| const onSynced = () => setStatus("saved"); |
| provider.on("connect", onConnect); |
| provider.on("disconnect", onDisconnect); |
| provider.on("synced", onSynced); |
| const wsProvider = (provider as any).configuration?.websocketProvider; |
| setStatus(wsProvider?.status === "connected" ? "saved" : "disconnected"); |
| return () => { |
| provider.off("connect", onConnect); |
| provider.off("disconnect", onDisconnect); |
| provider.off("synced", onSynced); |
| }; |
| }, [providerRef]); |
|
|
| useEffect(() => { |
| const editor = editorInstance; |
| const provider = providerRef.current; |
| if (!editor || !provider) return; |
| const onUpdate = () => { |
| setStatus("syncing"); |
| if (timerRef.current) clearTimeout(timerRef.current); |
| timerRef.current = setTimeout(() => { |
| const wsProvider = (provider as any).configuration?.websocketProvider; |
| setStatus(wsProvider?.status === "connected" ? "saved" : "disconnected"); |
| }, 1500); |
| }; |
| editor.on("update", onUpdate); |
| return () => { editor.off("update", onUpdate); }; |
| }, [editorInstance, providerRef]); |
|
|
| const label = |
| status === "saved" ? "Saved" : |
| status === "syncing" ? "Saving..." : |
| status === "connected" ? "Connected" : |
| "Offline"; |
|
|
| const tooltip = |
| status === "saved" ? "All changes saved" : |
| status === "syncing" ? "Saving..." : |
| status === "connected" ? "Connected" : |
| "Disconnected"; |
|
|
| return ( |
| <Tooltip title={tooltip}> |
| <span className={`sync-indicator sync-indicator--${status}`}> |
| {status === "syncing" && <Loader2 size={14} className="spin" />} |
| {status === "saved" && <Cloud size={14} />} |
| {status === "connected" && <Cloud size={14} />} |
| {status === "disconnected" && <CloudOff size={14} />} |
| <span className="sync-indicator__label">{label}</span> |
| </span> |
| </Tooltip> |
| ); |
| } |
|
|