| 1 | import { type ReactNode, createContext, useContext, useEffect, useMemo, useState } from "react"; |
| 2 | |
| 3 | import type { CustomEmoji, EmojiList, MemberProfile, Result } from "@g1t/contracts"; |
| 4 | |
| 5 | /** |
| 6 | * What emoji components need from the page around them: the workspace's |
| 7 | * own emoji, where their images are served, who is looking, and how to |
| 8 | * react to a message. A conversation provides it; without one, reactions |
| 9 | * show but cannot be clicked and only standard emoji are offered. |
| 10 | */ |
| 11 | export type EmojiContextValue = { |
| 12 | customs: CustomEmoji[]; |
| 13 | /** Each custom emoji's image file, by name. */ |
| 14 | byName: ReadonlyMap<string, string>; |
| 15 | /** The usercontent origin custom emoji images are served from. */ |
| 16 | usercontent: string; |
| 17 | me: MemberProfile | null; |
| 18 | /** Reacts to a message (`on`) or takes it back. */ |
| 19 | react?: (messageId: string, emoji: string, on: boolean) => void; |
| 20 | /** Where the workspace's emoji are managed, for the picker's "Add emoji". */ |
| 21 | manageHref?: string; |
| 22 | }; |
| 23 | |
| 24 | const EMPTY: EmojiContextValue = { customs: [], byName: new Map(), usercontent: "", me: null }; |
| 25 | |
| 26 | export const EmojiContext = createContext<EmojiContextValue>(EMPTY); |
| 27 | |
| 28 | export function useEmojiContext(): EmojiContextValue { |
| 29 | return useContext(EmojiContext); |
| 30 | } |
| 31 | |
| 32 | export function EmojiProvider({ value, children }: { value: Omit<EmojiContextValue, "byName">; children: ReactNode }) { |
| 33 | const byName = useMemo(() => new Map(value.customs.map((e) => [e.name, e.file])), [value.customs]); |
| 34 | const full = useMemo(() => ({ ...value, byName }), [value, byName]); |
| 35 | return <EmojiContext.Provider value={full}>{children}</EmojiContext.Provider>; |
| 36 | } |
| 37 | |
| 38 | /** Each workspace's emoji as last fetched, shared by every conversation in the tab. */ |
| 39 | const fetched = new Map<string, Promise<CustomEmoji[]>>(); |
| 40 | |
| 41 | /** Drops what was fetched, after the workspace's emoji change. */ |
| 42 | export function forgetCustomEmoji(slug: string): void { |
| 43 | fetched.delete(slug.toLowerCase()); |
| 44 | } |
| 45 | |
| 46 | /** The workspace's own emoji, fetched once per tab (routes/workspace/chat/api.ts, `?emoji=1`). */ |
| 47 | export function useCustomEmoji(slug: string): CustomEmoji[] { |
| 48 | const [list, setList] = useState<CustomEmoji[]>([]); |
| 49 | useEffect(() => { |
| 50 | if (!slug) return; |
| 51 | const key = slug.toLowerCase(); |
| 52 | let promise = fetched.get(key); |
| 53 | if (!promise) { |
| 54 | promise = fetch(`/${slug}/-/chat/api?emoji=1`) |
| 55 | .then((response) => response.json() as Promise<Result<EmojiList>>) |
| 56 | .then((result) => (result.ok ? result.value.emoji : [])) |
| 57 | .catch(() => { |
| 58 | fetched.delete(key); |
| 59 | return []; |
| 60 | }); |
| 61 | fetched.set(key, promise); |
| 62 | } |
| 63 | let live = true; |
| 64 | void promise.then((emoji) => live && setList(emoji)); |
| 65 | return () => { |
| 66 | live = false; |
| 67 | }; |
| 68 | }, [slug]); |
| 69 | return list; |
| 70 | } |