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);
+}