mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): stop pickers re-slicing their grid on first paint (#1898)
This commit is contained in:
1 parent
7a938c85f6
commit
670a3a970e
4 files changed
+52
-39
No files matched your search
@@ -67,23 +67,23 @@ export const GifPickerView = observer(({onClose, selectGif}: GifPickerProps = {}
|
|||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [estimatedContentSize, setEstimatedContentSize] = useState<number | null>(null);
|
const [estimatedContentSize, setEstimatedContentSize] = useState<number | null>(null);
|
||||||
useSearchInputAutofocus(searchInputRef);
|
useSearchInputAutofocus(searchInputRef);
|
||||||
const {viewportSize, scrollTop, handleScroll, handleResize, scrollToTop} = useScrollerViewport(scrollerRef);
|
const {viewportSize, scrollTop, handleScroll, handleResize, jumpToStartEdge} = useScrollerViewport(scrollerRef);
|
||||||
useWindowFocusVideoControl({scrollerRef, videoPool, gifAutoPlay});
|
useWindowFocusVideoControl({scrollerRef, videoPool});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
store.ensureFeaturedLoaded();
|
store.ensureFeaturedLoaded();
|
||||||
return () => store.dispose();
|
return () => store.dispose();
|
||||||
}, [store]);
|
}, [store]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToTop();
|
jumpToStartEdge();
|
||||||
}, [store.view, scrollToTop]);
|
}, [store.view, jumpToStartEdge]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
(!store.loading && store.shouldRenderSearchResults) ||
|
(!store.loading && store.shouldRenderSearchResults) ||
|
||||||
(!store.searchTerm.trim() && !store.shouldRenderSearchResults)
|
(!store.searchTerm.trim() && !store.shouldRenderSearchResults)
|
||||||
) {
|
) {
|
||||||
scrollToTop();
|
jumpToStartEdge();
|
||||||
}
|
}
|
||||||
}, [store.loading, store.shouldRenderSearchResults, store.searchTerm, scrollToTop]);
|
}, [store.loading, store.shouldRenderSearchResults, store.searchTerm, jumpToStartEdge]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!store.isShowingFavorites || FavoriteGif.totalCount === 0) return;
|
if (!store.isShowingFavorites || FavoriteGif.totalCount === 0) return;
|
||||||
void FavoriteGifCommands.refreshFavoriteGifPreviews();
|
void FavoriteGifCommands.refreshFavoriteGifPreviews();
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ export const MemesPickerView = observer(({onClose}: MemesPickerProps = {}) => {
|
|||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [estimatedContentSize, setEstimatedContentSize] = useState<number | null>(null);
|
const [estimatedContentSize, setEstimatedContentSize] = useState<number | null>(null);
|
||||||
useSearchInputAutofocus(searchInputRef);
|
useSearchInputAutofocus(searchInputRef);
|
||||||
const {viewportSize, scrollTop, handleScroll, handleResize, scrollToTop} = useScrollerViewport(scrollerRef);
|
const {viewportSize, scrollTop, handleScroll, handleResize, jumpToStartEdge} = useScrollerViewport(scrollerRef);
|
||||||
useWindowFocusVideoControl({scrollerRef, videoPool, gifAutoPlay: false});
|
useWindowFocusVideoControl({scrollerRef, videoPool});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToTop();
|
jumpToStartEdge();
|
||||||
}, [state.selectedFilter, state.searchTerm, scrollToTop]);
|
}, [state.selectedFilter, state.searchTerm, jumpToStartEdge]);
|
||||||
const filteredMemes = useMemo(() => {
|
const filteredMemes = useMemo(() => {
|
||||||
let memes = [...favoriteMemes];
|
let memes = [...favoriteMemes];
|
||||||
if (state.selectedFilter !== 'all') {
|
if (state.selectedFilter !== 'all') {
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import type {ScrollerHandle} from '@app/features/ui/components/Scroller';
|
import type {ScrollerHandle} from '@app/features/ui/components/Scroller';
|
||||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
type ResizeType = 'container' | 'content';
|
type ResizeType = 'container' | 'content';
|
||||||
|
|
||||||
|
function readContentBoxSize(element: HTMLElement): {width: number; height: number} | null {
|
||||||
|
const view = element.ownerDocument.defaultView;
|
||||||
|
if (view === null) return null;
|
||||||
|
const style = view.getComputedStyle(element);
|
||||||
|
const width = element.clientWidth - Number.parseFloat(style.paddingLeft) - Number.parseFloat(style.paddingRight);
|
||||||
|
const height = element.clientHeight - Number.parseFloat(style.paddingTop) - Number.parseFloat(style.paddingBottom);
|
||||||
|
if (!(width > 0) || !(height > 0)) return null;
|
||||||
|
return {width, height};
|
||||||
|
}
|
||||||
|
|
||||||
export function useScrollerViewport(scrollerRef: React.RefObject<ScrollerHandle | null>) {
|
export function useScrollerViewport(scrollerRef: React.RefObject<ScrollerHandle | null>) {
|
||||||
const [viewportSize, setViewportSize] = useState({width: 0, height: 0});
|
const [viewportSize, setViewportSize] = useState({width: 0, height: 0});
|
||||||
const [scrollTop, setScrollTop] = useState(0);
|
const [scrollTop, setScrollTop] = useState(0);
|
||||||
@@ -12,6 +22,7 @@ export function useScrollerViewport(scrollerRef: React.RefObject<ScrollerHandle
|
|||||||
const scrollFrameRef = useRef<number | null>(null);
|
const scrollFrameRef = useRef<number | null>(null);
|
||||||
const pendingViewportSizeRef = useRef<{width: number; height: number} | null>(null);
|
const pendingViewportSizeRef = useRef<{width: number; height: number} | null>(null);
|
||||||
const viewportFrameRef = useRef<number | null>(null);
|
const viewportFrameRef = useRef<number | null>(null);
|
||||||
|
const hasMeasuredViewportRef = useRef(false);
|
||||||
const flushScrollTop = useCallback(() => {
|
const flushScrollTop = useCallback(() => {
|
||||||
scrollFrameRef.current = null;
|
scrollFrameRef.current = null;
|
||||||
const nextScrollTop = pendingScrollTopRef.current;
|
const nextScrollTop = pendingScrollTopRef.current;
|
||||||
@@ -27,16 +38,25 @@ export function useScrollerViewport(scrollerRef: React.RefObject<ScrollerHandle
|
|||||||
},
|
},
|
||||||
[flushScrollTop],
|
[flushScrollTop],
|
||||||
);
|
);
|
||||||
const flushViewportSize = useCallback(() => {
|
const commitViewportSize = useCallback((nextViewportSize: {width: number; height: number}) => {
|
||||||
viewportFrameRef.current = null;
|
|
||||||
const nextViewportSize = pendingViewportSizeRef.current;
|
|
||||||
pendingViewportSizeRef.current = null;
|
pendingViewportSizeRef.current = null;
|
||||||
if (!nextViewportSize) return;
|
if (viewportFrameRef.current != null) {
|
||||||
|
cancelAnimationFrame(viewportFrameRef.current);
|
||||||
|
viewportFrameRef.current = null;
|
||||||
|
}
|
||||||
|
hasMeasuredViewportRef.current = true;
|
||||||
setViewportSize((prev) => {
|
setViewportSize((prev) => {
|
||||||
if (prev.width === nextViewportSize.width && prev.height === nextViewportSize.height) return prev;
|
if (prev.width === nextViewportSize.width && prev.height === nextViewportSize.height) return prev;
|
||||||
return nextViewportSize;
|
return nextViewportSize;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
const flushViewportSize = useCallback(() => {
|
||||||
|
viewportFrameRef.current = null;
|
||||||
|
const nextViewportSize = pendingViewportSizeRef.current;
|
||||||
|
pendingViewportSizeRef.current = null;
|
||||||
|
if (!nextViewportSize) return;
|
||||||
|
commitViewportSize(nextViewportSize);
|
||||||
|
}, [commitViewportSize]);
|
||||||
const scheduleViewportSize = useCallback(
|
const scheduleViewportSize = useCallback(
|
||||||
(nextViewportSize: {width: number; height: number}) => {
|
(nextViewportSize: {width: number; height: number}) => {
|
||||||
pendingViewportSizeRef.current = nextViewportSize;
|
pendingViewportSizeRef.current = nextViewportSize;
|
||||||
@@ -55,15 +75,22 @@ export function useScrollerViewport(scrollerRef: React.RefObject<ScrollerHandle
|
|||||||
(entry: ResizeObserverEntry, type: ResizeType) => {
|
(entry: ResizeObserverEntry, type: ResizeType) => {
|
||||||
if (type !== 'container') return;
|
if (type !== 'container') return;
|
||||||
const {width, height} = entry.contentRect;
|
const {width, height} = entry.contentRect;
|
||||||
|
if (!hasMeasuredViewportRef.current && width > 0 && height > 0) {
|
||||||
|
commitViewportSize({width, height});
|
||||||
|
return;
|
||||||
|
}
|
||||||
scheduleViewportSize({width, height});
|
scheduleViewportSize({width, height});
|
||||||
},
|
},
|
||||||
[scheduleViewportSize],
|
[commitViewportSize, scheduleViewportSize],
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const state = scrollerRef.current?.readViewportMetrics();
|
if (hasMeasuredViewportRef.current) return;
|
||||||
if (!state || state.offsetWidth === 0 || state.offsetHeight === 0) return;
|
const element = scrollerRef.current?.getViewportElement() ?? null;
|
||||||
scheduleViewportSize({width: state.offsetWidth, height: state.offsetHeight});
|
if (element === null) return;
|
||||||
}, [scrollerRef, scheduleViewportSize]);
|
const contentBox = readContentBoxSize(element);
|
||||||
|
if (contentBox === null) return;
|
||||||
|
commitViewportSize(contentBox);
|
||||||
|
}, [scrollerRef, commitViewportSize]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (scrollFrameRef.current != null) {
|
if (scrollFrameRef.current != null) {
|
||||||
@@ -74,7 +101,7 @@ export function useScrollerViewport(scrollerRef: React.RefObject<ScrollerHandle
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const scrollToTop = useCallback(() => {
|
const jumpToStartEdge = useCallback(() => {
|
||||||
scrollerRef.current?.scrollTo({to: 0, animate: false});
|
scrollerRef.current?.scrollTo({to: 0, animate: false});
|
||||||
pendingScrollTopRef.current = null;
|
pendingScrollTopRef.current = null;
|
||||||
if (scrollFrameRef.current != null) {
|
if (scrollFrameRef.current != null) {
|
||||||
@@ -89,6 +116,6 @@ export function useScrollerViewport(scrollerRef: React.RefObject<ScrollerHandle
|
|||||||
setScrollTop,
|
setScrollTop,
|
||||||
handleScroll,
|
handleScroll,
|
||||||
handleResize,
|
handleResize,
|
||||||
scrollToTop,
|
jumpToStartEdge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -16,30 +16,16 @@ interface VideoPoolControl {
|
|||||||
interface UseWindowFocusVideoControlOptions {
|
interface UseWindowFocusVideoControlOptions {
|
||||||
scrollerRef: React.RefObject<ScrollerHandle | null>;
|
scrollerRef: React.RefObject<ScrollerHandle | null>;
|
||||||
videoPool: VideoPoolControl;
|
videoPool: VideoPoolControl;
|
||||||
gifAutoPlay?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWindowFocusVideoControl({
|
export function useWindowFocusVideoControl({scrollerRef, videoPool}: UseWindowFocusVideoControlOptions): void {
|
||||||
scrollerRef,
|
|
||||||
videoPool,
|
|
||||||
gifAutoPlay = true,
|
|
||||||
}: UseWindowFocusVideoControlOptions): void {
|
|
||||||
const poolRef = useRef(videoPool);
|
const poolRef = useRef(videoPool);
|
||||||
poolRef.current = videoPool;
|
poolRef.current = videoPool;
|
||||||
const scrollerRefRef = useRef(scrollerRef);
|
const scrollerRefRef = useRef(scrollerRef);
|
||||||
scrollerRefRef.current = scrollerRef;
|
scrollerRefRef.current = scrollerRef;
|
||||||
const gifAutoPlayRef = useRef(gifAutoPlay);
|
|
||||||
gifAutoPlayRef.current = gifAutoPlay;
|
|
||||||
useEffect(() => {
|
|
||||||
if (gifAutoPlay && getAnimatedMediaPlaybackAllowed()) {
|
|
||||||
poolRef.current.resumeAll();
|
|
||||||
} else {
|
|
||||||
poolRef.current.pauseAll();
|
|
||||||
}
|
|
||||||
}, [gifAutoPlay]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updatePlayback = () => {
|
const updatePlayback = () => {
|
||||||
if (gifAutoPlayRef.current && getAnimatedMediaPlaybackAllowed()) {
|
if (getAnimatedMediaPlaybackAllowed()) {
|
||||||
poolRef.current.resumeAll();
|
poolRef.current.resumeAll();
|
||||||
} else {
|
} else {
|
||||||
poolRef.current.pauseAll();
|
poolRef.current.pauseAll();
|
||||||
|
|||||||
Reference in new issue
Block a user