81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
/**
|
|
* Renders a user's avatar, or their initial, wherever a user is named: cards,
|
|
* comments, chat, profiles and mention suggestions.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { useContext, useState } from "react";
|
|
import { imageProps, userApiUrl } from "../../utils/shared/urls.ts";
|
|
import { AvatarOverrideContext } from "../../contexts/ws/AvatarOverrideContext.ts";
|
|
|
|
/**
|
|
* Passed to {@link Avatar} from whatever data the caller has on the user, which
|
|
* may be stale: a live override from `AvatarOverrideContext` wins over
|
|
* `hasAvatar` and `version`.
|
|
*/
|
|
export interface AvatarProps {
|
|
userId: string;
|
|
username: string;
|
|
hasAvatar: boolean;
|
|
/** Sets the side, in pixels. */
|
|
size?: number;
|
|
/** Added to the URL so that a changed avatar isn't served from cache. */
|
|
version?: number;
|
|
}
|
|
|
|
/**
|
|
* Renders the image from `GET /api/users/:userId/avatar`, scaled by the server
|
|
* to the size it is shown at, or the user's initial when they have none or it
|
|
* fails to load. An avatar added, changed or removed
|
|
* since the caller's data was fetched shows at once, through the
|
|
* `user_updated` messages that `WSProvider` puts in `AvatarOverrideContext`.
|
|
*/
|
|
export function Avatar(
|
|
{ userId, username, hasAvatar, size = 36, version }: AvatarProps,
|
|
): React.JSX.Element {
|
|
const [imgFailed, setImgFailed] = useState(false);
|
|
const sizeStyle = { width: size, height: size };
|
|
|
|
const override = useContext(AvatarOverrideContext)[userId];
|
|
const effectiveHasAvatar = override ? override.hasAvatar : hasAvatar;
|
|
const effectiveVersion = override ? override.version : version;
|
|
|
|
// A new image deserves a new try, so a past load failure is cleared. It is
|
|
// done during render, since an effect would show the initial for a frame.
|
|
const imgKey = `${effectiveHasAvatar}:${effectiveVersion ?? ""}`;
|
|
const [prevImgKey, setPrevImgKey] = useState(imgKey);
|
|
if (prevImgKey !== imgKey) {
|
|
setPrevImgKey(imgKey);
|
|
setImgFailed(false);
|
|
}
|
|
|
|
if (effectiveHasAvatar && !imgFailed) {
|
|
const src = effectiveVersion
|
|
? `${userApiUrl({ id: userId }, "avatar")}?v=${effectiveVersion}`
|
|
: userApiUrl({ id: userId }, "avatar");
|
|
return (
|
|
<img
|
|
{...imageProps(src, size)}
|
|
alt={username}
|
|
title={username}
|
|
style={sizeStyle}
|
|
className="avatar-img"
|
|
loading="lazy"
|
|
decoding="async"
|
|
onError={() => setImgFailed(true)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="avatar-initials"
|
|
title={username}
|
|
style={{ ...sizeStyle, fontSize: size * 0.45 }}
|
|
>
|
|
{username.charAt(0).toUpperCase()}
|
|
</div>
|
|
);
|
|
}
|