Merge branch 'fix/fab-scroll-visibility' into 'main'
Hide FAB while ComposeBox is visible, fade in on scroll See merge request soapbox-pub/ditto!31
This commit is contained in:
+20
-2
@@ -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<HTMLDivElement>(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 (
|
||||
<main className="flex-1 min-w-0">
|
||||
{!hideCompose && <ComposeBox compact />}
|
||||
{!hideCompose && <div ref={composeRef}><ComposeBox compact /></div>}
|
||||
|
||||
{header}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
<div className={cn("relative flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l border-r border-border bg-background/85")}>
|
||||
<Outlet />
|
||||
{showFAB && (
|
||||
<div className="sticky bottom-fab sidebar:bottom-6 z-30 pointer-events-none flex justify-end pr-6">
|
||||
<div className="pointer-events-auto">
|
||||
<div className={cn(
|
||||
'sticky bottom-fab sidebar:bottom-6 z-30 pointer-events-none flex justify-end pr-6 transition-all duration-300',
|
||||
fabHidden ? 'opacity-0 translate-y-4' : 'opacity-100 translate-y-0',
|
||||
)}>
|
||||
<div className={cn('pointer-events-auto', fabHidden && 'pointer-events-none')}>
|
||||
<FloatingComposeButton kind={fabKind} href={fabHref} onFabClick={onFabClick} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<Listener>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user