From cba978090f422835843b498edaa94a465562ee38 Mon Sep 17 00:00:00 2001 From: Adrian <107351903+6lr61@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:42:17 +0200 Subject: [PATCH] feat: early third party emotes rendering support does not support modifiers or zero-width emotes --- src/components/Emotes.tsx | 4 +-- src/components/Message.tsx | 20 ++++++++++-- src/hooks/useBttvEmotes.ts | 10 +++--- src/hooks/useSevenTvEmotes.ts | 18 +++++------ src/hooks/useThirdPartyEmotes.ts | 55 ++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 src/hooks/useThirdPartyEmotes.ts diff --git a/src/components/Emotes.tsx b/src/components/Emotes.tsx index 83d0370..11769fd 100644 --- a/src/components/Emotes.tsx +++ b/src/components/Emotes.tsx @@ -14,7 +14,7 @@ export default function Emotes(): ReactElement { @@ -22,7 +22,7 @@ export default function Emotes(): ReactElement { diff --git a/src/components/Message.tsx b/src/components/Message.tsx index 7da9a57..55d2db8 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -1,5 +1,8 @@ +import { + useThirdPartyEmotes, + type Fragment, +} from "../hooks/useThirdPartyEmotes"; import type { ChatMessage } from "../hooks/useTwitchChat"; -import type { ChatFragment } from "../utils/event-sub/events/chat/message"; import BadgeList from "./BadgeList"; import ElapsedTime from "./ElapsedTime"; import MentionSegment from "./MentionSegment"; @@ -28,7 +31,7 @@ function colorToRgba(color?: string): string | undefined { } function fragmentToComponent( - fragment: ChatFragment, + fragment: Fragment, index: number ): React.ReactElement | undefined { const key = `fragment-${index.toString()}`; @@ -40,12 +43,23 @@ function fragmentToComponent( 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 (
@@ -61,7 +75,7 @@ export default function Message({ message }: Props): React.ReactElement { {message.reply && }
- {message.message.fragments.map((fragment, index) => + {fragments.map((fragment, index) => fragmentToComponent(fragment, index) )}
diff --git a/src/hooks/useBttvEmotes.ts b/src/hooks/useBttvEmotes.ts index ec57bfa..f06dd34 100644 --- a/src/hooks/useBttvEmotes.ts +++ b/src/hooks/useBttvEmotes.ts @@ -21,7 +21,7 @@ interface BetterTTVEmote { height?: number; } -interface BetterTTVEmoteFragment { +export interface BetterTTVEmoteFragment { type: "bttv-emote"; text: string; animated: boolean; @@ -32,12 +32,12 @@ interface BetterTTVEmoteFragment { name: string; }; small: { - file: string; + src: string; height: number; width: number; }; big: { - file: string; + src: string; height: number; width: number; }; @@ -62,12 +62,12 @@ function makeBttvFragments( name: emote.user.displayName, }, small: { - file: `https://cdn.betterttv.net/emote/${emote.id}/1x.webp`, + src: `https://cdn.betterttv.net/emote/${emote.id}/1x.webp`, height: emote.height ?? 28, width: emote.width ?? 28, }, big: { - file: `https://cdn.betterttv.net/emote/${emote.id}/3x.webp`, + src: `https://cdn.betterttv.net/emote/${emote.id}/3x.webp`, height: 3 * (emote.height ?? 28), width: 3 * (emote.width ?? 28), }, diff --git a/src/hooks/useSevenTvEmotes.ts b/src/hooks/useSevenTvEmotes.ts index 704f04a..5992caf 100644 --- a/src/hooks/useSevenTvEmotes.ts +++ b/src/hooks/useSevenTvEmotes.ts @@ -144,7 +144,7 @@ interface Connection { emote_set: EmoteSet | null; } -interface SevenTVEmoteFragment { +export interface SevenTVEmoteFragment { type: "7tv-emote"; text: string; animated: boolean; @@ -154,14 +154,14 @@ interface SevenTVEmoteFragment { name: string; }; small: { - file: string; - height?: number; - width?: number; + src: string; + height: number; + width: number; }; big: { - file: string; - height?: number; - width?: number; + src: string; + height: number; + width: number; }; } @@ -182,10 +182,10 @@ function filterEmote(emote: SevenTvEmoteModel): boolean { function sizeOf( filename: string, files: SevenTvEmoteModel["data"]["host"]["files"] -): { height?: number; width?: number } { +): { height: number; width: number } { const file = files.find(({ name }) => name === filename); - return { height: file?.height, width: file?.width }; + return { height: file?.height ?? 32, width: file?.width ?? 32 }; } function makeSevenTvFragments( diff --git a/src/hooks/useThirdPartyEmotes.ts b/src/hooks/useThirdPartyEmotes.ts new file mode 100644 index 0000000..f1a065b --- /dev/null +++ b/src/hooks/useThirdPartyEmotes.ts @@ -0,0 +1,55 @@ +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"; + +export type Fragment = + | ChatFragment + | BetterTTVEmoteFragment + | SevenTVEmoteFragment; + +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 }; + const names = Object.keys(emotes); + + return fragments.flatMap((fragment) => { + if (fragment.type !== "text") { + return fragment; + } + + const found = [...fragment.text.matchAll(/[!:\w]+/g)].filter( + ({ 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({ + type: "text", + text: fragment.text.slice(start, index), + }); + } + + subFragments.push(emotes[name]); + start = index + name.length; + } + + if (start < fragment.text.length) { + subFragments.push({ + type: "text", + text: fragment.text.slice(start), + }); + } + + return subFragments; + }); +}