From 2377a4c9d0278959ae0a1df273f3edebcd698f23 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 24 Aug 2026 12:50:06 +0200 Subject: [PATCH] fix(app): honour motion settings in the mobile meme picker (#1942) --- .../channel/components/MobileMemesPicker.tsx | 59 +++++++------------ 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/fluxer_app/src/features/channel/components/MobileMemesPicker.tsx b/fluxer_app/src/features/channel/components/MobileMemesPicker.tsx index fa75a0ac5..337f43233 100644 --- a/fluxer_app/src/features/channel/components/MobileMemesPicker.tsx +++ b/fluxer_app/src/features/channel/components/MobileMemesPicker.tsx @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import Accessibility from '@app/features/accessibility/state/Accessibility'; import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal'; import {LongPressable} from '@app/features/app/components/LongPressable'; import {useAnimatedMediaVideoPlayback} from '@app/features/app/hooks/useAnimatedMediaPlayback'; import {useSearchInputAutofocus} from '@app/features/app/hooks/useSearchInputAutofocus'; +import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate'; import styles from '@app/features/channel/components/GifPicker.module.css'; import memeStyles from '@app/features/channel/components/MemesPicker.module.css'; import {PickerEmptyState} from '@app/features/channel/components/shared/PickerEmptyState'; @@ -19,6 +19,8 @@ import type {FavoriteMeme} from '@app/features/expressions/models/FavoriteMeme'; import FavoriteMemes from '@app/features/expressions/state/FavoriteMemes'; import {AUDIO_DESCRIPTOR, GIFS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils'; +import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport'; +import {buildStaticGifPreviewURL} from '@app/features/messaging/utils/MediaProxyUtils'; import {ComponentBus} from '@app/features/platform/utils/ComponentBus'; import {DeleteIcon, EditIcon} from '@app/features/ui/action_menu/ContextMenuIcons'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; @@ -31,7 +33,7 @@ import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {GifIcon, ImageIcon, MusicNoteIcon, SmileySadIcon, VideoCameraIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; -import {AnimatePresence, motion} from 'framer-motion'; +import {AnimatePresence} from 'framer-motion'; import {matchSorter} from 'match-sorter'; import {observer} from 'mobx-react-lite'; import type React from 'react'; @@ -93,6 +95,8 @@ const EDIT_SAVED_MEDIA_DESCRIPTOR = msg({ comment: 'Button or menu action label in the channel and chat mobile memes picker. Keep it concise.', }); +const GRID_ITEM_PRELOAD_ROOT_MARGIN = '800px 0px'; + type ContentType = 'all' | 'image' | 'video' | 'audio' | 'gif'; interface FilterOption { @@ -414,14 +418,20 @@ const MemeGridRenderer = observer(({memes}: {memes: Array}) => { const GridItem = observer( ({title, onClick, onLongPress, url, width, height, contentType, duration, filename, isGifv}: GridItemProps) => { const {i18n} = useLingui(); - const [isVisible, setIsVisible] = useState(false); - const mediaRef = useRef(null); + const {ref: mediaRef, isNearViewport: isVisible} = useNearViewport({ + rememberKey: url, + rootMargin: GRID_ITEM_PRELOAD_ROOT_MARGIN, + }); const videoRef = useRef(null); const accessibleName = title || filename; - const isVideo = contentType.startsWith('video/') || contentType.includes('gif'); + const isVideo = contentType.startsWith('video/'); const isAudio = contentType.startsWith('audio/'); + const isGifImage = !isVideo && contentType.toLowerCase().includes('gif'); + const shouldAnimateGif = useShouldAnimate({kind: 'gif', isAnimated: !isAudio && (isVideo || isGifImage)}); + const thumbnailSrc = isGifImage && !shouldAnimateGif ? buildStaticGifPreviewURL(url) : url; const videoPlaybackAllowed = useAnimatedMediaVideoPlayback(videoRef, { enabled: isVisible && !isAudio && isVideo, + shouldPlay: shouldAnimateGif, }); const handleClick = () => { onClick(); @@ -431,27 +441,6 @@ const GridItem = observer( event.preventDefault(); handleClick(); }; - useEffect(() => { - const mediaElement = mediaRef.current; - if (!mediaElement) return; - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - setIsVisible(true); - } - }); - }, - { - rootMargin: '800px 0px', - threshold: 0, - }, - ); - observer.observe(mediaElement); - return () => { - observer.disconnect(); - }; - }, []); return ( - +
{isGifv && }
@@ -496,9 +478,8 @@ const GridItem = observer( {isVisible && !isAudio && !isVideo && ( {title} )} @@ -526,7 +507,7 @@ const GridItem = observer(
)}
-
+
); },