Skip to content
File

Blob: src/client/components/ui/emoji-icon.tsx

typescript56 lines
1import { useEffect, useState } from "react";
2import { getEmojiAssetUrlSync, loadEmojiAssetUrl, normalizeEmoji } from "@/client/lib/emoji";
3 
4interface EmojiIconProps {
5 emoji: string;
6 size?: number;
7}
8 
9export function EmojiIcon({ emoji, size = 20 }: EmojiIconProps) {
10 const cachedAssetUrl = getEmojiAssetUrlSync(emoji);
11 const [assetState, setAssetState] = useState(() => ({
12 emoji,
13 assetUrl: cachedAssetUrl,
14 failed: false,
15 }));
16 const currentAsset = assetState.emoji === emoji ? assetState : { emoji, assetUrl: cachedAssetUrl, failed: false };
17 const style = { width: size, height: size };
18 
19 useEffect(() => {
20 if (currentAsset.assetUrl !== null) return;
21 let cancelled = false;
22 loadEmojiAssetUrl(emoji).then((url) => {
23 if (!cancelled) setAssetState({ emoji, assetUrl: url, failed: false });
24 });
25 return () => {
26 cancelled = true;
27 };
28 }, [currentAsset.assetUrl, emoji]);
29 
30 if (currentAsset.assetUrl && !currentAsset.failed) {
31 return (
32 <img
33 src={currentAsset.assetUrl}
34 alt=""
35 aria-hidden
36 draggable={false}
37 decoding="async"
38 onError={() => setAssetState({ emoji, assetUrl: currentAsset.assetUrl, failed: true })}
39 className="inline-block shrink-0 select-none align-text-bottom"
40 style={style}
41 />
42 );
43 }
44 
45 return (
46 <span
47 role="img"
48 aria-label={normalizeEmoji(emoji)}
49 className="inline-flex shrink-0 select-none items-center justify-center align-text-bottom leading-none"
50 style={{ ...style, fontSize: size }}
51 >
52 {normalizeEmoji(emoji)}
53 </span>
54 );
55}