Skip to content

Commit 4badb37

Browse files
authored
Merge pull request #307 from SableClient/fix/animated-avatars
Make animated svgs loop
2 parents 888ab90 + 84ce6d7 commit 4badb37

2 files changed

Lines changed: 56 additions & 4 deletions

File tree

.changeset/fix-animated-avatars.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
Fix animated avatars not looping.

src/app/components/room-avatar/AvatarImage.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AvatarImage as FoldsAvatarImage } from 'folds';
2-
import { ReactEventHandler, useState } from 'react';
2+
import { ReactEventHandler, useState, useEffect } from 'react';
33
import bgColorImg from '$utils/bgColorImg';
44
import { settingsAtom } from '$state/settings';
55
import { useSetting } from '$state/hooks/settings';
@@ -15,20 +15,67 @@ type AvatarImageProps = {
1515
export function AvatarImage({ src, alt, uniformIcons, onError }: AvatarImageProps) {
1616
const [uniformIconsSetting] = useSetting(settingsAtom, 'uniformIcons');
1717
const [image, setImage] = useState<HTMLImageElement | undefined>(undefined);
18-
const normalizedBg = image ? bgColorImg(image) : undefined;
18+
const [processedSrc, setProcessedSrc] = useState<string>(src);
19+
1920
const useUniformIcons = uniformIconsSetting && uniformIcons === true;
21+
const normalizedBg = useUniformIcons && image ? bgColorImg(image) : undefined;
22+
23+
useEffect(() => {
24+
let isMounted = true;
25+
let objectUrl: string | null = null;
26+
27+
const processImage = async () => {
28+
try {
29+
const res = await fetch(src, { mode: 'cors' });
30+
const contentType = res.headers.get('content-type');
31+
32+
if (contentType && contentType.includes('image/svg+xml')) {
33+
const text = await res.text();
34+
const parser = new DOMParser();
35+
const doc = parser.parseFromString(text, 'image/svg+xml');
36+
37+
const animations = doc.querySelectorAll('animate, animateTransform, animateMotion');
38+
animations.forEach((anim) => anim.setAttribute('repeatCount', 'indefinite'));
39+
40+
const style = doc.createElementNS('http://www.w3.org/2000/svg', 'style');
41+
style.textContent = '* { animation-iteration-count: infinite !important; }';
42+
doc.documentElement.appendChild(style);
43+
44+
const serializer = new XMLSerializer();
45+
const newSvgString = serializer.serializeToString(doc);
46+
const blob = new Blob([newSvgString], { type: 'image/svg+xml' });
47+
48+
objectUrl = URL.createObjectURL(blob);
49+
if (isMounted) setProcessedSrc(objectUrl);
50+
} else if (isMounted) setProcessedSrc(src);
51+
} catch {
52+
if (isMounted) setProcessedSrc(src);
53+
}
54+
};
55+
56+
processImage();
57+
58+
return () => {
59+
isMounted = false;
60+
if (objectUrl) {
61+
URL.revokeObjectURL(objectUrl);
62+
}
63+
};
64+
}, [src]);
2065

2166
const handleLoad: ReactEventHandler<HTMLImageElement> = (evt) => {
2267
evt.currentTarget.setAttribute('data-image-loaded', 'true');
2368
setImage(evt.currentTarget);
2469
};
2570

71+
const isBlobUrl = processedSrc.startsWith('blob:');
72+
2673
return (
2774
<FoldsAvatarImage
2875
className={css.RoomAvatar}
2976
style={{ backgroundColor: useUniformIcons ? normalizedBg : undefined }}
30-
src={src}
31-
crossOrigin="anonymous"
77+
src={processedSrc}
78+
crossOrigin={isBlobUrl ? undefined : 'anonymous'}
3279
alt={alt}
3380
onError={() => {
3481
setImage(undefined);

0 commit comments

Comments
 (0)