"use client"; import type { PropsWithChildren } from "react"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useEditMode } from "@homarr/boards/edit-mode"; import { readSectionCollapsedFromStorage, writeSectionCollapsedToStorage } from "./section-collapse-storage"; interface SectionCollapseContextValue { collapsedSectionIds: ReadonlySet; setCollapsed: (sectionId: string, collapsed: boolean) => void; } const SectionCollapseContext = createContext(null); export const BoardSectionCollapseProvider = ({ children }: PropsWithChildren) => { const board = useRequiredBoard(); const { status } = useSession(); const { mutate } = clientApi.section.changeCollapsed.useMutation(); const [collapsedById, setCollapsedById] = useState>(() => Object.fromEntries( board.sections.filter((section) => section.kind !== "empty").map((section) => [section.id, section.collapsed]), ), ); const collapsedByIdRef = useRef(collapsedById); collapsedByIdRef.current = collapsedById; useEffect(() => { setCollapsedById((previous) => { const next = { ...previous }; for (const section of board.sections) { if (section.kind === "empty" || section.id in next) continue; next[section.id] = section.collapsed; } return next; }); }, [board.sections]); useEffect(() => { if (status !== "unauthenticated") return; setCollapsedById((previous) => { const next = { ...previous }; for (const sectionId of Object.keys(next)) { next[sectionId] = readSectionCollapsedFromStorage(window.localStorage, sectionId, next[sectionId] ?? false); } return next; }); }, [status]); const setCollapsed = useCallback( (sectionId: string, collapsed: boolean) => { const previousCollapsed = collapsedByIdRef.current[sectionId] ?? false; collapsedByIdRef.current = { ...collapsedByIdRef.current, [sectionId]: collapsed }; setCollapsedById((previous) => ({ ...previous, [sectionId]: collapsed })); if (status === "authenticated") { mutate( { sectionId, collapsed }, { onError: () => { if (collapsedByIdRef.current[sectionId] !== collapsed) return; collapsedByIdRef.current = { ...collapsedByIdRef.current, [sectionId]: previousCollapsed }; setCollapsedById((previous) => ({ ...previous, [sectionId]: previousCollapsed })); }, }, ); } else if (status === "unauthenticated") { writeSectionCollapsedToStorage(window.localStorage, sectionId, collapsed); } }, [mutate, status], ); const value = useMemo( () => ({ collapsedSectionIds: new Set( Object.entries(collapsedById) .filter(([, collapsed]) => collapsed) .map(([sectionId]) => sectionId), ), setCollapsed, }), [collapsedById, setCollapsed], ); return {children}; }; export const useSectionCollapse = ({ sectionId, collapsible }: { sectionId: string; collapsible: boolean }) => { const context = useContext(SectionCollapseContext); const [isEditMode] = useEditMode(); if (!context) { throw new Error("BoardSectionCollapseProvider is required"); } const isCollapsed = collapsible && context.collapsedSectionIds.has(sectionId); return { isCollapsed, isVisuallyCollapsed: isCollapsed && !isEditMode, setCollapsed: (collapsed: boolean) => context.setCollapsed(sectionId, collapsed), toggle: () => context.setCollapsed(sectionId, !isCollapsed), }; }; export const useCollapsedSectionIds = () => { const context = useContext(SectionCollapseContext); const [isEditMode] = useEditMode(); if (!context) { throw new Error("BoardSectionCollapseProvider is required"); } return isEditMode ? EMPTY_SET : context.collapsedSectionIds; }; const EMPTY_SET = new Set();