56 lines
2.5 KiB
TypeScript
56 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
|
|
|
/**
|
|
* Disclosure state for the queue panel. A pinned session starts open (the
|
|
* caller renders nothing while the queue is empty, so the flag only becomes
|
|
* visible once entries exist, including entries that arrive asynchronously
|
|
* after mount); an unpinned session starts collapsed. The panel collapses on
|
|
* a full drain and follows the target session's pin on session switch.
|
|
* Collapsing via the header X closes it for the current view; a later mount
|
|
* reopens it while the pin is retained. A pin turning on mid-view (another
|
|
* tab's storage event) opens the queue when there is something to show;
|
|
* unpinning never closes an already-open panel. A pinned queue that was
|
|
* empty reopens when entries arrive (initial async load, session switch, or
|
|
* a new message after a full drain); ordinary count changes on a nonempty
|
|
* queue never reopen an X-collapsed panel. An empty queue starts collapsed
|
|
* even when pinned, so unpinning before delayed entries arrive leaves it
|
|
* collapsed. The pin is desktop-only: on phone viewports it is treated as
|
|
* off (the header hides the toggle there, so a pin stored at desktop width
|
|
* must not auto-open the panel with no way to unpin). State is adjusted
|
|
* during render (React docs: "Adjusting some state when a prop changes") to
|
|
* avoid the cascading-render anti-pattern of doing it inside useEffect.
|
|
*/
|
|
export function useQueuePanelOpenState(
|
|
sessionId: string | null,
|
|
entryCount: number,
|
|
pinned: boolean,
|
|
) {
|
|
const { isMobile } = useResponsiveBreakpoint();
|
|
const effectivePinned = isMobile ? false : pinned;
|
|
const [isOpen, setIsOpen] = useState(effectivePinned && entryCount > 0);
|
|
const [lastSession, setLastSession] = useState(sessionId);
|
|
const [lastEntryCount, setLastEntryCount] = useState(entryCount);
|
|
const [lastPinned, setLastPinned] = useState(effectivePinned);
|
|
if (sessionId !== lastSession) {
|
|
setLastSession(sessionId);
|
|
setIsOpen(effectivePinned && entryCount > 0);
|
|
}
|
|
if (entryCount !== lastEntryCount) {
|
|
const wasEmpty = lastEntryCount === 0;
|
|
setLastEntryCount(entryCount);
|
|
if (entryCount === 0) {
|
|
setIsOpen(false);
|
|
} else if (effectivePinned && wasEmpty) {
|
|
setIsOpen(true);
|
|
}
|
|
}
|
|
if (effectivePinned !== lastPinned) {
|
|
setLastPinned(effectivePinned);
|
|
if (effectivePinned && entryCount > 0) setIsOpen(true);
|
|
}
|
|
return [isOpen, setIsOpen] as const;
|
|
}
|