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 {
{Object.entries(bttvEmotes).map(([name, emote]) => (
-
-
+
))}
@@ -22,7 +22,7 @@ export default function Emotes(): ReactElement {
{Object.entries(sevenTvEmotes).map(([name, emote]) => (
-
-
+
))}
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;
+ });
+}