mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): key-based React remount for workspace switching (#415)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { Workspace } from "./types";
|
||||
import {
|
||||
loadActiveWorkspaceId,
|
||||
loadWorkspaces,
|
||||
saveActiveWorkspaceId,
|
||||
saveWorkspaces,
|
||||
} from "./workspaceStorage";
|
||||
|
||||
export type UseWorkspacesReturn = {
|
||||
workspaces: Workspace[];
|
||||
activeWorkspace: Workspace | null;
|
||||
/** Counter bumped when the active workspace's config changes (relayUrl/token). */
|
||||
reinitKey: number;
|
||||
/** Add a workspace, deduplicating by relayUrl. Returns the final ID in the list. */
|
||||
addWorkspace: (workspace: Workspace) => string;
|
||||
removeWorkspace: (id: string) => void;
|
||||
switchWorkspace: (id: string) => void;
|
||||
updateWorkspace: (
|
||||
id: string,
|
||||
updates: Partial<Pick<Workspace, "name" | "relayUrl" | "token">>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
const WorkspacesContext = createContext<UseWorkspacesReturn | null>(null);
|
||||
|
||||
export function WorkspacesProvider({ children }: { children: ReactNode }) {
|
||||
const value = useWorkspacesInternal();
|
||||
return (
|
||||
<WorkspacesContext.Provider value={value}>
|
||||
{children}
|
||||
</WorkspacesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWorkspaces(): UseWorkspacesReturn {
|
||||
const ctx = useContext(WorkspacesContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useWorkspaces must be used within a WorkspacesProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function useWorkspacesInternal(): UseWorkspacesReturn {
|
||||
const [workspaces, setWorkspacesState] =
|
||||
useState<Workspace[]>(loadWorkspaces);
|
||||
const [activeId, setActiveId] = useState<string | null>(
|
||||
loadActiveWorkspaceId,
|
||||
);
|
||||
const [reinitKey, setReinitKey] = useState(0);
|
||||
const workspacesRef = useRef(workspaces);
|
||||
workspacesRef.current = workspaces;
|
||||
|
||||
const activeWorkspace = useMemo(
|
||||
() => workspaces.find((w) => w.id === activeId) ?? workspaces[0] ?? null,
|
||||
[workspaces, activeId],
|
||||
);
|
||||
|
||||
const addWorkspace = useCallback((workspace: Workspace): string => {
|
||||
const existing = workspacesRef.current.find(
|
||||
(w) => w.relayUrl === workspace.relayUrl,
|
||||
);
|
||||
const resolvedId = existing?.id ?? workspace.id;
|
||||
setWorkspacesState((prev) => {
|
||||
const dup = prev.find((w) => w.relayUrl === workspace.relayUrl);
|
||||
let next: Workspace[];
|
||||
if (dup) {
|
||||
next = prev.map((w) =>
|
||||
w.id === dup.id
|
||||
? {
|
||||
...w,
|
||||
name: workspace.name || w.name,
|
||||
token: workspace.token ?? w.token,
|
||||
nsec: workspace.nsec ?? w.nsec,
|
||||
pubkey: workspace.pubkey ?? w.pubkey,
|
||||
}
|
||||
: w,
|
||||
);
|
||||
} else {
|
||||
next = [...prev, workspace];
|
||||
}
|
||||
saveWorkspaces(next);
|
||||
return next;
|
||||
});
|
||||
return resolvedId;
|
||||
}, []);
|
||||
|
||||
const removeWorkspace = useCallback(
|
||||
(id: string) => {
|
||||
setWorkspacesState((prev) => {
|
||||
// Never allow removing the last workspace
|
||||
if (prev.length <= 1) {
|
||||
return prev;
|
||||
}
|
||||
const next = prev.filter((w) => w.id !== id);
|
||||
saveWorkspaces(next);
|
||||
|
||||
// If removing the active workspace, switch to first remaining
|
||||
if (activeId === id && next.length > 0) {
|
||||
saveActiveWorkspaceId(next[0].id);
|
||||
setActiveId(next[0].id);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[activeId],
|
||||
);
|
||||
|
||||
const switchWorkspace = useCallback(
|
||||
(id: string) => {
|
||||
if (id === activeId) {
|
||||
return;
|
||||
}
|
||||
saveActiveWorkspaceId(id);
|
||||
setActiveId(id);
|
||||
},
|
||||
[activeId],
|
||||
);
|
||||
|
||||
const updateWorkspace = useCallback(
|
||||
(
|
||||
id: string,
|
||||
updates: Partial<Pick<Workspace, "name" | "relayUrl" | "token">>,
|
||||
) => {
|
||||
setWorkspacesState((prev) => {
|
||||
// Prevent duplicate relay URLs across workspaces
|
||||
if (
|
||||
updates.relayUrl &&
|
||||
prev.some((w) => w.id !== id && w.relayUrl === updates.relayUrl)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
const next = prev.map((w) => (w.id === id ? { ...w, ...updates } : w));
|
||||
saveWorkspaces(next);
|
||||
return next;
|
||||
});
|
||||
// If the active workspace's relay URL or token changed, bump reinitKey
|
||||
// so the React tree remounts with the new config.
|
||||
if (
|
||||
id === activeId &&
|
||||
(updates.relayUrl || updates.token !== undefined)
|
||||
) {
|
||||
setReinitKey((k) => k + 1);
|
||||
}
|
||||
},
|
||||
[activeId],
|
||||
);
|
||||
|
||||
return {
|
||||
workspaces,
|
||||
activeWorkspace,
|
||||
reinitKey,
|
||||
addWorkspace,
|
||||
removeWorkspace,
|
||||
switchWorkspace,
|
||||
updateWorkspace,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user