signal-desktop/ts/components/BetterAvatar.tsx

119 lines
3 KiB
TypeScript
Raw Normal View History

2021-08-06 00:17:05 +00:00
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MouseEvent } from 'react';
import React, { useEffect, useState } from 'react';
2021-08-06 00:17:05 +00:00
import { noop } from 'lodash';
import type { AvatarDataType } from '../types/Avatar';
2021-08-06 00:17:05 +00:00
import { BetterAvatarBubble } from './BetterAvatarBubble';
import type { LocalizerType } from '../types/Util';
2021-08-06 00:17:05 +00:00
import { Spinner } from './Spinner';
2021-09-24 00:49:05 +00:00
import { avatarDataToBytes } from '../util/avatarDataToBytes';
2021-08-06 00:17:05 +00:00
type AvatarSize = 48 | 80;
export type PropsType = {
avatarData: AvatarDataType;
i18n: LocalizerType;
isSelected?: boolean;
2021-09-24 00:49:05 +00:00
onClick: (avatarBuffer: Uint8Array | undefined) => unknown;
2021-08-06 00:17:05 +00:00
onDelete: () => unknown;
size?: AvatarSize;
};
2022-11-18 00:45:19 +00:00
export function BetterAvatar({
2021-08-06 00:17:05 +00:00
avatarData,
i18n,
isSelected,
onClick,
onDelete,
size = 48,
2022-11-18 00:45:19 +00:00
}: PropsType): JSX.Element {
2021-09-24 00:49:05 +00:00
const [avatarBuffer, setAvatarBuffer] = useState<Uint8Array | undefined>(
2021-08-06 00:17:05 +00:00
avatarData.buffer
);
const [avatarURL, setAvatarURL] = useState<string | undefined>(undefined);
useEffect(() => {
let shouldCancel = false;
async function makeAvatar() {
2021-09-24 00:49:05 +00:00
const buffer = await avatarDataToBytes(avatarData);
2021-08-06 00:17:05 +00:00
if (!shouldCancel) {
setAvatarBuffer(buffer);
}
}
// If we don't have this we'll get lots of flashing because avatarData
// changes too much. Once we have a buffer set we don't need to reload.
if (avatarBuffer) {
return noop;
}
void makeAvatar();
2021-08-06 00:17:05 +00:00
return () => {
shouldCancel = true;
};
}, [avatarBuffer, avatarData]);
2021-09-24 00:49:05 +00:00
// Convert avatar's Uint8Array to a URL object
2021-08-06 00:17:05 +00:00
useEffect(() => {
if (avatarBuffer) {
const url = URL.createObjectURL(new Blob([avatarBuffer]));
setAvatarURL(url);
}
}, [avatarBuffer]);
// Clean up any remaining object URLs
useEffect(() => {
return () => {
if (avatarURL) {
URL.revokeObjectURL(avatarURL);
}
};
}, [avatarURL]);
const isEditable = Boolean(avatarData.color);
const handleDelete = !avatarData.icon
? (ev: MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
onDelete();
}
: undefined;
return (
<BetterAvatarBubble
i18n={i18n}
isSelected={isSelected}
onDelete={handleDelete}
onSelect={() => {
onClick(avatarBuffer);
}}
style={{
backgroundImage: avatarURL ? `url(${avatarURL})` : undefined,
backgroundSize: size,
2022-02-09 20:33:19 +00:00
// +8 so that the size is the actual size we want, 8 is the invisible
2021-08-06 00:17:05 +00:00
// padding around the bubble to make room for the selection border
height: size + 8,
width: size + 8,
}}
>
{isEditable && isSelected && (
<div className="BetterAvatarBubble--editable" />
)}
{!avatarURL && (
<div className="module-Avatar__spinner-container">
<Spinner
size={`${size - 8}px`}
svgSize="small"
direction="on-avatar"
/>
</div>
)}
</BetterAvatarBubble>
);
2022-11-18 00:45:19 +00:00
}