Files
cinny/src/app/components/room-avatar/RoomAvatar.tsx
litruv c286501be8 Improve timeline scroll, media layout, avatars, and emoji coverage.
Remember room scroll position across switches, reserve image/video heights from Matrix dimensions, cache authenticated media blobs for avatars, restore jumbo emoji-only messages, and extend emoji font unicode-range for Unicode 15 glyphs.
2026-07-22 20:00:14 +10:00

153 lines
4.4 KiB
TypeScript

import { JoinRule } from 'matrix-js-sdk';
import { AvatarFallback, AvatarImage, color } from 'folds';
import { Icon, Icons } from '../icons';
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useEffect, useState } from 'react';
import { Blurhash } from 'react-blurhash';
import * as css from './RoomAvatar.css';
import { joinRuleToIconSrc } from '../../utils/room';
import colorMXID from '../../../util/colorMXID';
import { useAuthenticatedMediaUrl } from '../../hooks/useAuthenticatedMediaUrl';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { cacheAvatar, isAvatarCached, pruneAvatarCache } from '../../utils/avatarCache';
import {
getCachedAuthenticatedMediaUrl,
isAuthenticatedMediaUrl,
} from '../../utils/authenticatedMediaCache';
import {
getMediaBlurHash,
getMediaDimensions,
rememberMediaBlurHash,
} from '../../state/mediaDimensionCache';
type RoomAvatarProps = {
roomId: string;
src?: string;
alt?: string;
renderFallback: () => ReactNode;
};
export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps) {
const [error, setError] = useState(false);
const useAuthentication = useMediaAuthentication();
const authenticatedSrc = useAuthenticatedMediaUrl(src, useAuthentication);
const blurHash = getMediaBlurHash(src ?? '');
const dims = getMediaDimensions(src ?? '');
const blobCached =
!!src &&
(!useAuthentication ||
!isAuthenticatedMediaUrl(src) ||
!!getCachedAuthenticatedMediaUrl(src));
const [loaded, setLoaded] = useState(() => isAvatarCached(src) || blobCached);
useEffect(() => {
if (
authenticatedSrc &&
(isAvatarCached(src) ||
(src && (!useAuthentication || !isAuthenticatedMediaUrl(src) || getCachedAuthenticatedMediaUrl(src))))
) {
setLoaded(true);
}
}, [authenticatedSrc, src, useAuthentication]);
const handleLoad: ReactEventHandler<HTMLImageElement> = (evt) => {
evt.currentTarget.setAttribute('data-image-loaded', 'true');
setLoaded(true);
cacheAvatar(src);
pruneAvatarCache();
rememberMediaBlurHash(src ?? '', evt.currentTarget);
};
if (!src || error) {
return (
<AvatarFallback
style={{ backgroundColor: colorMXID(roomId ?? ''), color: color.Surface.Container }}
className={css.RoomAvatar}
>
{renderFallback()}
</AvatarFallback>
);
}
if (authenticatedSrc && loaded) {
return (
<AvatarImage
className={css.RoomAvatar}
src={authenticatedSrc}
alt={alt}
onError={() => setError(true)}
onLoad={handleLoad}
draggable={false}
data-image-loaded="true"
/>
);
}
const blurResolutionX = 32;
const blurResolutionY =
dims?.w && dims?.h && dims.w > 0
? Math.max(1, Math.round(blurResolutionX * (dims.h / dims.w)))
: 32;
return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<AvatarFallback
style={{
backgroundColor: colorMXID(roomId ?? ''),
color: color.Surface.Container,
position: 'absolute',
inset: 0,
}}
className={css.RoomAvatar}
>
{renderFallback()}
</AvatarFallback>
{blurHash && (
<div
style={{
position: 'absolute',
inset: 0,
overflow: 'hidden',
borderRadius: 'inherit',
filter: 'blur(6px)',
transform: 'scale(1.08)',
}}
>
<Blurhash
hash={blurHash}
width="100%"
height="100%"
resolutionX={blurResolutionX}
resolutionY={blurResolutionY}
punch={1.1}
style={{ display: 'block' }}
/>
</div>
)}
{authenticatedSrc && (
<AvatarImage
className={css.RoomAvatar}
style={{ position: 'relative', zIndex: 1, opacity: loaded ? 1 : 0 }}
src={authenticatedSrc}
alt={alt}
onError={() => setError(true)}
onLoad={handleLoad}
draggable={false}
/>
)}
</div>
);
}
export const RoomIcon = forwardRef<
SVGSVGElement,
Omit<ComponentProps<typeof Icon>, 'src'> & {
joinRule: JoinRule;
space?: boolean;
}
>(({ joinRule, space, ...props }, ref) => (
<Icon
src={joinRuleToIconSrc(Icons, joinRule, space || false) ?? Icons.Hash}
{...props}
ref={ref}
/>
));