diff --git a/src/components/Message.tsx b/src/components/Message.tsx index 2bab48a..30bb82a 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -4,6 +4,7 @@ import BadgeList from "./BadgeList"; import ElapsedTime from "./ElapsedTime"; import MentionSegment from "./MentionSegment"; import ProfilePicture from "./ProfilePicture"; +import Pronoun from "./Pronoun"; import Reply from "./Reply"; import TextSegment from "./TextSegment"; import TwitchEmote from "./TwitchEmote"; @@ -54,7 +55,8 @@ export default function Message({ message }: Props): React.ReactElement { style={{ backgroundColor: colorToRgba(message.color) }} > - + + {message.reply && } diff --git a/src/components/Pronoun.tsx b/src/components/Pronoun.tsx new file mode 100644 index 0000000..6029569 --- /dev/null +++ b/src/components/Pronoun.tsx @@ -0,0 +1,12 @@ +import type { ReactElement } from "react"; +import { usePronouns } from "../hooks/usePronouns"; + +export default function Pronoun({ + login, +}: { + login: string; +}): ReactElement | undefined { + const pronoun = usePronouns(login); + + return

{pronoun}

; +} diff --git a/src/components/UserName.tsx b/src/components/UserName.tsx index 882e083..33da112 100644 --- a/src/components/UserName.tsx +++ b/src/components/UserName.tsx @@ -10,7 +10,7 @@ export default function UserName({ message: ChatMessage; }): React.ReactElement { return ( -

+

{message.chatter_user_name} {hasLocalizedName(message) && ( ({message.chatter_user_login}) diff --git a/src/hooks/usePronouns.ts b/src/hooks/usePronouns.ts new file mode 100644 index 0000000..f42cab0 --- /dev/null +++ b/src/hooks/usePronouns.ts @@ -0,0 +1,103 @@ +import { useEffect, useMemo, useState } from "react"; + +const PRONOUNS_URL = "https://api.pronouns.alejo.io/v1"; + +interface PronounDescription { + name: string; + subject: string; + object: string; + singular: boolean; +} + +interface UserEntry { + channel_id: string; + channel_login: string; + pronoun_id: string; + alt_pronoun_id: null; // unused? +} + +type PronounResponse = Record; + +async function getPronouns(): Promise { + const response = await fetch(`${PRONOUNS_URL}/pronouns`); + + if (!response.ok) { + throw new Error( + `getUser: Bad HTTP response: ${response.status.toString()} ${ + response.statusText + }` + ); + } + + return (await response.json()) as PronounResponse; +} + +async function getUser(login: string): Promise { + const response = await fetch(`${PRONOUNS_URL}/users/${login}`); + + if (response.status === 404) { + return null; + } + + if (!response.ok) { + throw new Error( + `getUser: Bad HTTP response: ${response.status.toString()} ${ + response.statusText + }` + ); + } + + return (await response.json()) as UserEntry; +} + +function toString(description: PronounDescription): string { + return description.singular + ? description.subject + : `${description.subject}/${description.object}`; +} + +const descriptors = new Map(); +const users = new Map(); + +// FIXME DON'T DO THIS! +(function init() { + if (descriptors.size > 0) { + return; + } + + getPronouns() + .then((pronouns) => { + Object.entries(pronouns).forEach(([pronoun, description]) => + descriptors.set(pronoun, description) + ); + }) + .catch((error: unknown) => { + console.error("PronounProvider:", error); + }); +})(); + +export function usePronouns(login: string): string | undefined { + const [pronoun, setPronoun] = useState(); + const user = useMemo(() => users.get(login), [login]); + + if (user) { + setPronoun(() => descriptors.get(user.pronoun_id)); + } + + useEffect(() => { + if (user || user === null) { + return; + } + + void getUser(login).then((entry) => { + if (entry === null) { + users.set(login, null); + return; + } + + setPronoun(() => descriptors.get(entry.pronoun_id)); + }); + }, [login, user]); + + return pronoun && toString(pronoun); +}