diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx index 3c3bad50..5cc31e5e 100644 --- a/src/components/Feed.tsx +++ b/src/components/Feed.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useInView } from 'react-intersection-observer'; import { useQueryClient } from '@tanstack/react-query'; import { ComposeBox } from '@/components/ComposeBox'; @@ -15,6 +15,7 @@ import { useInfiniteSortedPosts } from '@/hooks/useTrending'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useMuteList } from '@/hooks/useMuteList'; import { isEventMuted } from '@/lib/muteHelpers'; +import { useSetFabHidden } from '@/contexts/LayoutContext'; import { cn } from '@/lib/utils'; import type { FeedItem } from '@/lib/feedUtils'; @@ -38,6 +39,23 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage }: F const { user } = useCurrentUser(); const { muteItems } = useMuteList(); const queryClient = useQueryClient(); + const setFabHidden = useSetFabHidden(); + + // Hide FAB while the ComposeBox is visible in the viewport + const composeRef = useRef(null); + useEffect(() => { + const el = composeRef.current; + if (!el || hideCompose) return; + const observer = new IntersectionObserver( + ([entry]) => setFabHidden(entry.isIntersecting), + { threshold: 0 }, + ); + observer.observe(el); + return () => { + observer.disconnect(); + setFabHidden(false); // restore FAB when Feed unmounts + }; + }, [hideCompose, setFabHidden]); // Tab settings from localStorage const showGlobalFeed = (() => { @@ -160,7 +178,7 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage }: F return (
- {!hideCompose && } + {!hideCompose &&
} {header} diff --git a/src/components/MainLayout.tsx b/src/components/MainLayout.tsx index 9828497c..3bba1595 100644 --- a/src/components/MainLayout.tsx +++ b/src/components/MainLayout.tsx @@ -64,7 +64,7 @@ function PageSkeleton() { /** Inner component that reads layout options from the context store. */ function MainLayoutInner() { - const { rightSidebar, showFAB = false, fabKind = 1, fabHref, onFabClick, noBottomSpacer = false, wrapperClassName } = useLayoutSnapshot(); + const { rightSidebar, showFAB = false, fabKind = 1, fabHref, onFabClick, noBottomSpacer = false, wrapperClassName, fabHidden } = useLayoutSnapshot(); const [drawerOpen, setDrawerOpen] = useState(false); const { config } = useAppContext(); @@ -92,8 +92,11 @@ function MainLayoutInner() {
{showFAB && ( -
-
+
+
diff --git a/src/contexts/LayoutContext.ts b/src/contexts/LayoutContext.ts index 444d919d..f82bece5 100644 --- a/src/contexts/LayoutContext.ts +++ b/src/contexts/LayoutContext.ts @@ -26,11 +26,22 @@ const EMPTY: LayoutOptions = {}; * A mutable store that holds the current layout options. * Pages call `setOptions` to update, and MainLayout subscribes via useSyncExternalStore. */ +/** Snapshot returned by the layout store, combining page options with dynamic state. */ +export type LayoutSnapshot = LayoutOptions & { fabHidden: boolean }; + +const EMPTY_SNAPSHOT: LayoutSnapshot = { fabHidden: false }; + export class LayoutStore { private options: LayoutOptions = EMPTY; + private _fabHidden = false; + private _snapshot: LayoutSnapshot = EMPTY_SNAPSHOT; private listeners = new Set(); - getSnapshot = (): LayoutOptions => this.options; + private buildSnapshot(): void { + this._snapshot = { ...this.options, fabHidden: this._fabHidden }; + } + + getSnapshot = (): LayoutSnapshot => this._snapshot; subscribe = (listener: Listener): (() => void) => { this.listeners.add(listener); @@ -40,12 +51,22 @@ export class LayoutStore { setOptions = (next: LayoutOptions): void => { if (this.options === next) return; this.options = next; + this.buildSnapshot(); + this.listeners.forEach((l) => l()); + }; + + setFabHidden = (hidden: boolean): void => { + if (this._fabHidden === hidden) return; + this._fabHidden = hidden; + this.buildSnapshot(); this.listeners.forEach((l) => l()); }; reset = (): void => { - if (this.options === EMPTY) return; + if (this.options === EMPTY && !this._fabHidden) return; this.options = EMPTY; + this._fabHidden = false; + this._snapshot = EMPTY_SNAPSHOT; this.listeners.forEach((l) => l()); }; } @@ -101,7 +122,13 @@ export function useLayoutOptions(options: LayoutOptions): void { } /** Hook for MainLayout to read the current layout options reactively. */ -export function useLayoutSnapshot(): LayoutOptions { +export function useLayoutSnapshot(): LayoutSnapshot { const store = useLayoutStore(); return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot); } + +/** Hook for components to signal that the FAB should be hidden (e.g., ComposeBox is visible). */ +export function useSetFabHidden(): (hidden: boolean) => void { + const store = useLayoutStore(); + return store.setFabHidden; +}