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.
153 lines
4.4 KiB
TypeScript
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}
|
|
/>
|
|
));
|