Skip to content

Commit

Permalink
feat: third party emotes
Browse files Browse the repository at this point in the history
  • Loading branch information
6lr61 committed Sep 18, 2024
1 parent 393e5c7 commit ef90110
Show file tree
Hide file tree
Showing 10 changed files with 847 additions and 0 deletions.
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export default tseslint.config(
{ allowConstantExport: true },
],
"@typescript-eslint/consistent-type-imports": ["error"],
"@typescript-eslint/prefer-literal-enum-member": [
"error",
{ allowBitwiseExpressions: true },
],
},
}
);
31 changes: 31 additions & 0 deletions src/components/Emotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useContext, type ReactElement } from "react";
import { useBttvEmotes } from "../hooks/useBttvEmotes";
import { AuthContext } from "../contexts/auth-state/AuthContext";
import { useSevenTvEmotes } from "../hooks/useSevenTvEmotes";

export default function Emotes(): ReactElement {
const { authState } = useContext(AuthContext);
const bttvEmotes = useBttvEmotes(authState?.user.id);
const sevenTvEmotes = useSevenTvEmotes(authState?.user.id);

return (
<article>
<h2>BetterTTV</h2>
<ul className="flex flex-row flex-wrap gap-1">
{Object.entries(bttvEmotes).map(([name, emote]) => (
<li key={name}>
<img alt={name} src={emote.small.file} {...emote.small} />
</li>
))}
</ul>
<h2>SevenTV</h2>
<ul className="flex flex-row flex-wrap gap-1">
{Object.entries(sevenTvEmotes).map(([name, emote]) => (
<li key={name}>
<img alt={name} src={emote.small.file} {...emote.small} />
</li>
))}
</ul>
</article>
);
}
116 changes: 116 additions & 0 deletions src/hooks/useBttvEmotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";

interface BetterTTVEmote {
id: string;
code: string;
imageType: string;
animated: boolean;
/** Used for global emotes */
userId?: string;
modifier?: boolean;
/** Used for shared and channel emotes */
user?: {
id: string;
name: string;
displayName: string;
providerId: string;
};
/** Omitted if the emote is 28x28 */
width?: number;
height?: number;
}

interface BetterTTVEmoteFragment {
type: "bttv-emote";
text: string;
animated: boolean;
global: boolean;
owner?: {
login: string;
name: string;
};
small: {
file: string;
height: number;
width: number;
};
big: {
file: string;
height: number;
width: number;
};
}

function makeBttvFragments(
emotes: BetterTTVEmote[],
global?: boolean
): Record<string, BetterTTVEmoteFragment> {
const entries = emotes.map(
(emote) =>
[
emote.code,
{
type: "bttv-emote",
text: emote.code,
animated: emote.animated,
global: global ?? false,
owner: emote.user && {
login: emote.user.name,
name: emote.user.displayName,
},
small: {
file: `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`,
height: 3 * (emote.height ?? 28),
width: 3 * (emote.width ?? 28),
},
},
] as const
);

return Object.fromEntries(entries);
}

const makeGlobalFragments = (emotes: BetterTTVEmote[]) =>
makeBttvFragments(emotes, true);

const makeChannelFragments = (emotes: BetterTTVEmote[]) =>
makeBttvFragments(emotes, false);

// TODO: Add some kind of runtime type checking here
export function useBttvEmotes(userId?: string) {
const { data: global } = useQuery({
queryKey: ["bttvGlobalEmotes"],
queryFn: () =>
fetch("https://api.betterttv.net/3/cached/emotes/global").then(
(response) => response.json()
),
select: makeGlobalFragments,
});

const { data: channel } = useQuery({
enabled: !!userId,
queryKey: ["bttvChannelEmotes", userId],
queryFn: () =>
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
fetch(`https://api.betterttv.net/3/cached/users/twitch/${userId}`)
.then((response) => response.json())
.then(
({
channelEmotes,
sharedEmotes,
}: {
channelEmotes: BetterTTVEmote[];
sharedEmotes: BetterTTVEmote[];
}) => [...channelEmotes, ...sharedEmotes]
),
select: makeChannelFragments,
});

return useMemo(() => ({ ...global, ...channel }), [global, channel]);
}
Loading

0 comments on commit ef90110

Please sign in to comment.