import { useEffect, useRef, useState } from "react"; import { relayClient } from "@/shared/api/relayClient"; import { applyWorkspace, getDefaultRelayUrl, getIdentity, } from "@/shared/api/tauri"; import { resetMediaCaches } from "@/shared/lib/mediaUrl"; import { clearSearchHitEventCache } from "@/app/navigation/searchHitEventCache"; import { clearAllDrafts } from "@/features/messages/lib/useDrafts"; import { resetAgentObserverStore } from "@/features/agents/observerRelayStore"; import { resetSidebarRelayConnectionCardState } from "@/features/sidebar/ui/useSidebarRelayConnectionCard"; import { resetVideoPlayerState } from "@/shared/ui/videoPlayerState"; import { initFirstWorkspace } from "./workspaceStorage"; import type { Workspace } from "./types"; /** * Tear down all workspace-scoped module singletons so the new * workspace starts with a clean slate. Hook-managed singletons * (e.g. ChannelMuteSyncManager, ChannelSectionSyncManager) are * destroyed via effect cleanup and do not need entries here. * See AGENTS.md "Workspace Switching" for the full contract. */ function resetWorkspaceState(): void { relayClient.disconnect(); resetAgentObserverStore(); resetSidebarRelayConnectionCardState(); resetMediaCaches(); resetVideoPlayerState(); clearSearchHitEventCache(); clearAllDrafts(); } type WorkspaceInitResult = | { isReady: true; needsSetup: false; appliedKey: string } | { isReady: false; needsSetup: true; defaultRelayUrl: string; } | { isReady: false; needsSetup: false; appliedKey: string | null }; /** * Applies the active workspace config to the Tauri backend and resets * all workspace-scoped module singletons when the workspace changes. * * Returns a discriminated union — only render the app after the * workspace is applied. When `needsSetup` is true, the caller * should show a first-run welcome screen. */ export function useWorkspaceInit( activeWorkspace: Workspace | null, workspaceKey: string, isSharedIdentity: boolean, ): WorkspaceInitResult { const [result, setResult] = useState({ isReady: false, needsSetup: false, appliedKey: null, }); // Track whether this is the initial mount or a workspace switch. // On the initial mount we skip resetting singletons (they're fresh). const hasInitializedRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: we intentionally depend on specific properties (id/relayUrl/token) — depending on the whole object would trigger resets on name-only changes useEffect(() => { let cancelled = false; async function init() { if (!activeWorkspace) { try { const defaultRelayUrl = await getDefaultRelayUrl(); if (isSharedIdentity) { const identity = await getIdentity(); if (cancelled) return; initFirstWorkspace(defaultRelayUrl, identity.pubkey); if (!cancelled) { window.location.reload(); } return; } if (!cancelled) { setResult({ isReady: false, needsSetup: true, defaultRelayUrl, }); } } catch { if (!cancelled) { setResult({ isReady: false, needsSetup: true, defaultRelayUrl: "ws://localhost:3000", }); } } return; } // Mark this workspace config as pending while it is applied to the // backend. App.tsx also checks appliedKey against the active workspaceKey, // which prevents rendering workspace-scoped UI for a new workspace until // that exact config has finished applying. setResult({ isReady: false, needsSetup: false, appliedKey: workspaceKey, }); // On workspace switch (not initial mount), reset module singletons // so the new tree starts with a clean slate. if (hasInitializedRef.current) { resetWorkspaceState(); } hasInitializedRef.current = true; // Apply workspace config to the Tauri backend. // // Note: we deliberately do NOT pass an nsec here. The persisted // `identity.key` file (resolved at startup by `resolve_persisted_identity`, // and updated atomically by `import_identity`) is the single source of // truth for the active key. Older builds stored the nsec in localStorage // and re-applied it on every reload, which silently overwrote any // imported key. `loadWorkspaces()` strips lingering `nsec` fields from // legacy entries; this site refuses to apply one even if present. try { await applyWorkspace( activeWorkspace.relayUrl, undefined, activeWorkspace.token, ); } catch (error) { console.error("Failed to apply workspace to backend:", error); } if (!cancelled) { setResult({ isReady: true, needsSetup: false, appliedKey: workspaceKey, }); } } void init(); return () => { cancelled = true; }; }, [ activeWorkspace?.id, activeWorkspace?.relayUrl, activeWorkspace?.token, isSharedIdentity, workspaceKey, ]); return result; }