From 299172c8aa0b2f71b310498ce299c08b83d2de6a Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 24 Aug 2026 12:47:59 +0200 Subject: [PATCH] fix(app): animate guild icons through the shared motion gate (#1931) --- .../components/popouts/GuildIcon.module.css | 5 -- .../guild/components/popouts/GuildIcon.tsx | 58 +++++-------------- 2 files changed, 15 insertions(+), 48 deletions(-) diff --git a/fluxer_app/src/features/guild/components/popouts/GuildIcon.module.css b/fluxer_app/src/features/guild/components/popouts/GuildIcon.module.css index 7fade651c..24a462dec 100644 --- a/fluxer_app/src/features/guild/components/popouts/GuildIcon.module.css +++ b/fluxer_app/src/features/guild/components/popouts/GuildIcon.module.css @@ -17,7 +17,6 @@ background-image: var(--guild-icon-image, none); background-position: center; background-size: cover; - transition: background-image 0.3s ease-in-out; container-type: inline-size; --initials-font-size: clamp(0.85rem, 45cqi, 1.35rem); --initials-letter-spacing: 0px; @@ -54,10 +53,6 @@ --initials-letter-spacing: 0px; } -.container[data-reduced-motion='true'] { - transition: none; -} - @container (max-width: 32px) { .initials { --initials-scale-cap: 0.88; diff --git a/fluxer_app/src/features/guild/components/popouts/GuildIcon.tsx b/fluxer_app/src/features/guild/components/popouts/GuildIcon.tsx index e97e92469..9c4ac75dc 100644 --- a/fluxer_app/src/features/guild/components/popouts/GuildIcon.tsx +++ b/fluxer_app/src/features/guild/components/popouts/GuildIcon.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import Accessibility from '@app/features/accessibility/state/Accessibility'; import {useHover} from '@app/features/app/hooks/useHover'; +import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate'; import styles from '@app/features/guild/components/popouts/GuildIcon.module.css'; import {getGuildIconDisplayInitials, getInitialsLength} from '@app/features/guild/utils/GuildInitialsUtils'; import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils'; @@ -30,12 +30,6 @@ type GuildIconStyleVars = React.CSSProperties & { '--guild-icon-image'?: string; }; -function resolveInitiallyLoadedImageUrl(url: string | null): string | null { - if (url == null) return null; - if (ImageCacheUtils.hasImage(url)) return url; - return null; -} - export const GuildIcon = observer(function GuildIcon({ id, name, @@ -48,57 +42,35 @@ export const GuildIcon = observer(function GuildIcon({ const initials = useMemo(() => getGuildIconDisplayInitials(rawInitials), [rawInitials]); const initialsLength = useMemo(() => getInitialsLength(rawInitials), [rawInitials]); const [hoverRef, isHovering] = useHover(); - const iconUrl = useMemo(() => (icon ? AvatarUtils.getGuildIconURL({id, icon}) : null), [id, icon]); - const hoverIconUrl = useMemo(() => (icon ? AvatarUtils.getGuildIconURL({id, icon}, true) : null), [id, icon]); - const [loadedStaticUrl, setLoadedStaticUrl] = useState(() => resolveInitiallyLoadedImageUrl(iconUrl)); - const [loadedAnimatedUrl, setLoadedAnimatedUrl] = useState(() => - resolveInitiallyLoadedImageUrl(hoverIconUrl), - ); - const isStaticLoaded = iconUrl != null && loadedStaticUrl === iconUrl; + const iconUrl = icon ? AvatarUtils.getGuildIconURL({id, icon}) : null; + const hoverIconUrl = icon ? AvatarUtils.getGuildIconURL({id, icon}, true) : null; + const isAnimatable = hoverIconUrl != null && hoverIconUrl !== iconUrl; + const animationAllowed = useShouldAnimate({kind: 'guild_icon', isAnimated: isAnimatable, isHovering}); + const [loadedAnimatedUrl, setLoadedAnimatedUrl] = useState(null); const isAnimatedLoaded = hoverIconUrl != null && loadedAnimatedUrl === hoverIconUrl; useEffect(() => { - if (iconUrl == null || iconUrl.length === 0 || isStaticLoaded) return; - let active = true; - const cleanup = ImageCacheUtils.loadImage(iconUrl, () => { - if (active) setLoadedStaticUrl(iconUrl); - }); - return () => { - active = false; - cleanup(); - }; - }, [iconUrl, isStaticLoaded]); - useEffect(() => { - if (!isHovering || hoverIconUrl == null || hoverIconUrl.length === 0 || isAnimatedLoaded) return; - let active = true; - const cleanup = ImageCacheUtils.loadImage(hoverIconUrl, () => { - if (active) setLoadedAnimatedUrl(hoverIconUrl); - }); - return () => { - active = false; - cleanup(); - }; - }, [isHovering, hoverIconUrl, isAnimatedLoaded]); - const shouldPlayAnimated = isHovering && isAnimatedLoaded; - const activeUrl = shouldPlayAnimated && hoverIconUrl != null ? hoverIconUrl : iconUrl; + if (!animationAllowed || hoverIconUrl == null || isAnimatedLoaded) return; + return ImageCacheUtils.loadImage(hoverIconUrl, () => setLoadedAnimatedUrl(hoverIconUrl)); + }, [animationAllowed, hoverIconUrl, isAnimatedLoaded]); + const activeUrl = animationAllowed && isAnimatedLoaded ? hoverIconUrl : iconUrl; + const paintedUrl = activeUrl != null && activeUrl.length > 0 ? activeUrl : null; const styleVars: GuildIconStyleVars = {}; if (sizePx != null) { styleVars['--guild-icon-size'] = remFromPx(sizePx); } - if (isStaticLoaded && activeUrl) { - styleVars['--guild-icon-image'] = `url(${activeUrl})`; + if (paintedUrl != null) { + styleVars['--guild-icon-image'] = `url(${paintedUrl})`; } - const reducedMotion = Accessibility.useReducedMotion; return (
- {!icon && ( + {paintedUrl == null && ( {initials}