import { createContext, useContext, useEffect, useRef, useSyncExternalStore } from 'react'; /** Options that pages can set to configure the persistent MainLayout. */ export interface LayoutOptions { /** Optional custom right sidebar to replace the default one */ rightSidebar?: React.ReactNode; /** Whether to show the floating compose button (default: false) */ showFAB?: boolean; /** The Nostr event kind the FAB creates (default: 1). Only used when showFAB is true. */ fabKind?: number; /** If set, the FAB navigates to this URL instead of opening a compose dialog. */ fabHref?: string; /** If set, overrides the default FAB click behavior. */ onFabClick?: () => void; /** If set, overrides the default FAB icon (Plus). */ fabIcon?: React.ReactNode; /** Additional classes for the wrapper div */ wrapperClassName?: string; /** * Optional scroll container element for the MobileBottomNav hide-on-scroll * behavior. Pages that scroll an internal container (e.g. Vines snap-scroll) * should set this so the bottom nav detects scroll direction correctly. */ scrollContainer?: HTMLElement | null; /** * If true, disables the bottom overscroll padding on the center column. * Use for pages with fixed-height layouts (chat, vines, livestream, etc.) * that manage their own scroll containers. */ noOverscroll?: boolean; /** * If true, removes the max-width constraint on the center column so it * expands to fill available space. Use with `rightSidebar: null` for * full-width page layouts (e.g. messaging). */ noMaxWidth?: boolean; } type Listener = () => void; const EMPTY: LayoutOptions = {}; /** * A mutable store that holds the current layout options. * Pages call `setOptions` to update, and MainLayout subscribes via useSyncExternalStore. */ export class LayoutStore { private options: LayoutOptions = EMPTY; private listeners = new Set(); getSnapshot = (): LayoutOptions => this.options; getOptions = (): LayoutOptions => this.options; subscribe = (listener: Listener): (() => void) => { this.listeners.add(listener); return () => this.listeners.delete(listener); }; setOptions = (next: LayoutOptions): void => { if (this.options === next) return; this.options = next; this.listeners.forEach((l) => l()); }; reset = (): void => { if (this.options === EMPTY) return; this.options = EMPTY; this.listeners.forEach((l) => l()); }; } export const LayoutStoreContext = createContext(null); function useLayoutStore(): LayoutStore { const store = useContext(LayoutStoreContext); if (!store) throw new Error('useLayoutOptions must be used within LayoutStoreContext'); return store; } /** * Hook for pages to declare their layout options. * Call this at the top of a page component to configure the surrounding MainLayout. * * Sets options synchronously during render so the layout never shows stale values. * Resets to defaults on unmount so options don't leak to the next page. */ export function useLayoutOptions(options: LayoutOptions): void { const store = useLayoutStore(); const prev = useRef(null); // Set options synchronously during render (before commit) so the layout // picks them up in the same paint as the new page. const changed = prev.current === null || prev.current.showFAB !== options.showFAB || prev.current.fabKind !== options.fabKind || prev.current.fabHref !== options.fabHref || prev.current.onFabClick !== options.onFabClick || prev.current.fabIcon !== options.fabIcon || prev.current.wrapperClassName !== options.wrapperClassName || prev.current.rightSidebar !== options.rightSidebar || prev.current.scrollContainer !== options.scrollContainer || prev.current.noOverscroll !== options.noOverscroll || prev.current.noMaxWidth !== options.noMaxWidth; if (changed) { prev.current = options; store.setOptions(options); } // Clean up on unmount — reset to defaults so the next page starts fresh. // Only reset if the store still holds this component's options. // During page transitions the new page's render-phase setOptions runs // before the old page's cleanup effect, so blindly resetting would // clobber the incoming page's options (causing the FAB to disappear). useEffect(() => { return () => { if (store.getOptions() === prev.current) { store.reset(); } }; }, [store]); } /** Hook for MainLayout to read the current layout options reactively. */ export function useLayoutSnapshot(): LayoutOptions { const store = useLayoutStore(); return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot); }