diff --git a/src/App.tsx b/src/App.tsx index bd1cb28..ce21243 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useContext, useMemo } from "react"; import { AuthContext } from "./contexts/auth-state/AuthContext"; import LoginButton from "./components/LoginButton"; -import Chat from "./components/Chat"; +import Messages from "./components/Messages"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export default function App() { @@ -23,7 +23,7 @@ export default function App() {

Hello: {authState.user.login}

Chat Messages:

- +
)} diff --git a/src/components/BadgeList.tsx b/src/components/BadgeList.tsx index b4ded3a..01da5c1 100644 --- a/src/components/BadgeList.tsx +++ b/src/components/BadgeList.tsx @@ -1,4 +1,4 @@ -import { useBadges } from "../hooks/useBadges"; +import type { useBadges } from "../hooks/useBadges"; interface Props { // FIXME: Derive this from a single point of truth @@ -8,6 +8,7 @@ interface Props { /** Months subscribed */ info: string; }[]; + twitchBadges: ReturnType; } function makeKey(setId: string, id: string): `${string}/${string}` { @@ -16,9 +17,8 @@ function makeKey(setId: string, id: string): `${string}/${string}` { export default function BadgeList({ badges, + twitchBadges, }: Props): React.ReactElement | undefined { - const twitchBadges = useBadges(); - if (badges.length === 0) { return; } diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx deleted file mode 100644 index c89c4be..0000000 --- a/src/components/Chat.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect, useRef } from "react"; -import { useTwitchChat } from "../hooks/useTwitchChat"; -import Message from "./Message"; - -export default function Chat(): React.ReactElement { - const messages = useTwitchChat(); - const ref = useRef(null); - - useEffect(() => { - if (!ref.current) { - return; - } - - ref.current.scroll(0, ref.current.scrollHeight); - }, [messages]); - - return ( -
-
- {messages.map((message) => ( - - ))} -
-
- ); -} diff --git a/src/components/Message.tsx b/src/components/Message.tsx deleted file mode 100644 index e0a4dc2..0000000 --- a/src/components/Message.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { - useThirdPartyEmotes, - type Fragment, -} from "../hooks/useThirdPartyEmotes"; -import type { ChatMessage } from "../hooks/useTwitchChat"; -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"; -import UserName from "./UserName"; - -interface Props { - message: ChatMessage; -} - -function colorToRgba(color?: string): string | undefined { - if (!color) { - return; - } - - // #aabbcc - const red = Number.parseInt(color.slice(1, 3), 16).toString(); - const green = Number.parseInt(color.slice(3, 5), 16).toString(); - const blue = Number.parseInt(color.slice(5), 16).toString(); - - return `rgba(${red}, ${green}, ${blue}, 0.25)`; -} - -function fragmentToComponent( - fragment: Fragment, - index: number -): React.ReactElement | undefined { - const key = `fragment-${index.toString()}`; - - switch (fragment.type) { - case "text": - return ; - case "emote": - return ; - case "mention": - return ; - case "7tv-emote": - case "bttv-emote": - return ( - - ); - default: - return; - } -} - -export default function Message({ message }: Props): React.ReactElement { - const fragments = useThirdPartyEmotes(message.message.fragments); - - return ( -
- -
-
- - - - -
- {message.reply && } -
- {fragments - .slice(message.reply ? 1 : 0) // Drop the first mention fragment - .map((fragment, index) => fragmentToComponent(fragment, index))} -
-
-
- ); -} diff --git a/src/components/Messages.tsx b/src/components/Messages.tsx new file mode 100644 index 0000000..4960631 --- /dev/null +++ b/src/components/Messages.tsx @@ -0,0 +1,128 @@ +import { useCallback, useContext, useEffect, useMemo, useRef } from "react"; +import { useTwitchChat } from "../hooks/useTwitchChat"; +import ProfilePicture from "./ProfilePicture"; +import BadgeList from "./BadgeList"; +import UserName from "./UserName"; +import Pronoun from "./Pronoun"; +import ElapsedTime from "./ElapsedTime"; +import Reply from "./Reply"; +import TextSegment from "./TextSegment"; +import TwitchEmote from "./TwitchEmote"; +import MentionSegment from "./MentionSegment"; +import { + findThirdPartyEmotes, + type Fragment, +} from "../utils/findThirdPartyEmotes"; +import { useBadges } from "../hooks/useBadges"; +import type { ChatFragment } from "../utils/event-sub/events/chat/message"; +import { useBttvEmotes } from "../hooks/useBttvEmotes"; +import { useSevenTvEmotes } from "../hooks/useSevenTvEmotes"; +import { AuthContext } from "../contexts/auth-state/AuthContext"; + +function colorToRgba(color?: string): string | undefined { + if (!color) { + return; + } + + const red = Number.parseInt(color.slice(1, 3), 16).toString(); + const green = Number.parseInt(color.slice(3, 5), 16).toString(); + const blue = Number.parseInt(color.slice(5), 16).toString(); + + return `rgba(${red}, ${green}, ${blue}, 0.25)`; +} + +function fragmentToComponent( + fragment: Fragment, + index: number +): React.ReactElement | undefined { + const key = `fragment-${index.toString()}`; + + switch (fragment.type) { + case "text": + return ; + case "emote": + return ; + case "mention": + return ; + case "7tv-emote": + case "bttv-emote": + return ( + + ); + default: + return; + } +} + +export default function Messages(): React.ReactElement { + const { authState } = useContext(AuthContext); + const messages = useTwitchChat(); + const ref = useRef(null); + const twitchBadges = useBadges(); + const bttvEmotes = useBttvEmotes(authState?.user.id); + const sevenTvEmotes = useSevenTvEmotes(authState?.user.id); + const emotes = useMemo( + () => ({ ...bttvEmotes, ...sevenTvEmotes }), + [bttvEmotes, sevenTvEmotes] + ); + const parseFragments = useCallback( + (fragments: ChatFragment[]) => findThirdPartyEmotes(fragments, emotes), + [emotes] + ); + + useEffect(() => { + if (!ref.current) { + return; + } + + ref.current.scroll(0, ref.current.scrollHeight); + }, [messages]); + + return ( +
+
+ {messages.map( + ({ + chatter_user_login, + chatter_user_name, + color, + badges, + timestamp, + reply, + message, + }) => ( +
+ +
+
+ + + + +
+ {reply && } +
+ {parseFragments(message.fragments) + .slice(reply ? 1 : 0) // Drop the first mention fragment + .map((fragment, index) => + fragmentToComponent(fragment, index) + )} +
+
+
+ ) + )} +
+
+ ); +} diff --git a/src/components/UserName.tsx b/src/components/UserName.tsx index 33da112..5404ae2 100644 --- a/src/components/UserName.tsx +++ b/src/components/UserName.tsx @@ -1,19 +1,15 @@ -import type { ChatMessage } from "../hooks/useTwitchChat"; - -function hasLocalizedName(message: ChatMessage): boolean { - return message.chatter_user_name.toLowerCase() !== message.chatter_user_login; -} - export default function UserName({ - message, + displayName, + login, }: { - message: ChatMessage; + displayName: string; + login: string; }): React.ReactElement { return (

- {message.chatter_user_name} - {hasLocalizedName(message) && ( - ({message.chatter_user_login}) + {displayName} + {displayName.toLowerCase() !== login && ( + ({login}) )}

); diff --git a/src/hooks/useThirdPartyEmotes.ts b/src/utils/findThirdPartyEmotes.ts similarity index 56% rename from src/hooks/useThirdPartyEmotes.ts rename to src/utils/findThirdPartyEmotes.ts index f1a065b..d8c595f 100644 --- a/src/hooks/useThirdPartyEmotes.ts +++ b/src/utils/findThirdPartyEmotes.ts @@ -1,22 +1,17 @@ -import { useContext } from "react"; -import { AuthContext } from "../contexts/auth-state/AuthContext"; -import { useBttvEmotes, type BetterTTVEmoteFragment } from "./useBttvEmotes"; -import { - useSevenTvEmotes, - type SevenTVEmoteFragment, -} from "./useSevenTvEmotes"; -import type { ChatFragment } from "../utils/event-sub/events/chat/message"; +import { type BetterTTVEmoteFragment } from "../hooks/useBttvEmotes"; +import { type SevenTVEmoteFragment } from "../hooks/useSevenTvEmotes"; +import type { ChatFragment } from "./event-sub/events/chat/message"; export type Fragment = | ChatFragment | BetterTTVEmoteFragment | SevenTVEmoteFragment; +type Emotes = Record; -export function useThirdPartyEmotes(fragments: ChatFragment[]): Fragment[] { - const { authState } = useContext(AuthContext); - const bttvEmotes = useBttvEmotes(authState?.user.id); - const sevenTvEmotes = useSevenTvEmotes(authState?.user.id); - const emotes = { ...bttvEmotes, ...sevenTvEmotes }; +export function findThirdPartyEmotes( + fragments: ChatFragment[], + emotes: Emotes +): Fragment[] { const names = Object.keys(emotes); return fragments.flatMap((fragment) => { @@ -28,9 +23,9 @@ export function useThirdPartyEmotes(fragments: ChatFragment[]): Fragment[] { ({ 0: match }) => names.includes(match) ); - /// Some text Kappa Kappa and such const subFragments: Fragment[] = []; let start = 0; + for (const { 0: name, index } of found) { if (start < index) { subFragments.push({