From 84ce6d7572ab8cd0fa1941cb01e7c10e3973fb87 Mon Sep 17 00:00:00 2001 From: 7w1 Date: Mon, 16 Mar 2026 14:59:36 -0500 Subject: [PATCH] there had to have been a better way to do this --- .changeset/fix-animated-avatars.md | 5 ++ .../components/room-avatar/AvatarImage.tsx | 55 +++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-animated-avatars.md diff --git a/.changeset/fix-animated-avatars.md b/.changeset/fix-animated-avatars.md new file mode 100644 index 000000000..9f3dc6797 --- /dev/null +++ b/.changeset/fix-animated-avatars.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix animated avatars not looping. diff --git a/src/app/components/room-avatar/AvatarImage.tsx b/src/app/components/room-avatar/AvatarImage.tsx index e9072fb0b..1e4cb0d0a 100644 --- a/src/app/components/room-avatar/AvatarImage.tsx +++ b/src/app/components/room-avatar/AvatarImage.tsx @@ -1,5 +1,5 @@ import { AvatarImage as FoldsAvatarImage } from 'folds'; -import { ReactEventHandler, useState } from 'react'; +import { ReactEventHandler, useState, useEffect } from 'react'; import bgColorImg from '$utils/bgColorImg'; import { settingsAtom } from '$state/settings'; import { useSetting } from '$state/hooks/settings'; @@ -15,20 +15,67 @@ type AvatarImageProps = { export function AvatarImage({ src, alt, uniformIcons, onError }: AvatarImageProps) { const [uniformIconsSetting] = useSetting(settingsAtom, 'uniformIcons'); const [image, setImage] = useState(undefined); - const normalizedBg = image ? bgColorImg(image) : undefined; + const [processedSrc, setProcessedSrc] = useState(src); + const useUniformIcons = uniformIconsSetting && uniformIcons === true; + const normalizedBg = useUniformIcons && image ? bgColorImg(image) : undefined; + + useEffect(() => { + let isMounted = true; + let objectUrl: string | null = null; + + const processImage = async () => { + try { + const res = await fetch(src, { mode: 'cors' }); + const contentType = res.headers.get('content-type'); + + if (contentType && contentType.includes('image/svg+xml')) { + const text = await res.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(text, 'image/svg+xml'); + + const animations = doc.querySelectorAll('animate, animateTransform, animateMotion'); + animations.forEach((anim) => anim.setAttribute('repeatCount', 'indefinite')); + + const style = doc.createElementNS('http://www.w3.org/2000/svg', 'style'); + style.textContent = '* { animation-iteration-count: infinite !important; }'; + doc.documentElement.appendChild(style); + + const serializer = new XMLSerializer(); + const newSvgString = serializer.serializeToString(doc); + const blob = new Blob([newSvgString], { type: 'image/svg+xml' }); + + objectUrl = URL.createObjectURL(blob); + if (isMounted) setProcessedSrc(objectUrl); + } else if (isMounted) setProcessedSrc(src); + } catch { + if (isMounted) setProcessedSrc(src); + } + }; + + processImage(); + + return () => { + isMounted = false; + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; + }, [src]); const handleLoad: ReactEventHandler = (evt) => { evt.currentTarget.setAttribute('data-image-loaded', 'true'); setImage(evt.currentTarget); }; + const isBlobUrl = processedSrc.startsWith('blob:'); + return ( { setImage(undefined);