From 1b433b4f70544739ca992aa04205d1ccc92a9409 Mon Sep 17 00:00:00 2001 From: mjfwebb Date: Thu, 25 Sep 2025 17:04:46 +0200 Subject: [PATCH 1/2] fix: move key to fragment --- src/views/Chat/ChatImageRenderer.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/views/Chat/ChatImageRenderer.tsx b/src/views/Chat/ChatImageRenderer.tsx index 67d6c1a..2b37f24 100644 --- a/src/views/Chat/ChatImageRenderer.tsx +++ b/src/views/Chat/ChatImageRenderer.tsx @@ -1,7 +1,6 @@ import twemoji from '@twemoji/api'; import classNames from 'classnames'; - -import { JSX } from 'react'; +import { Fragment, JSX } from 'react'; import { store } from '../../store/store'; import { ChannelChatMessageEvent } from '../../types/twitchEvents'; import { ChatCheer, ChatEmote } from '../../types/types'; @@ -183,10 +182,9 @@ export const ChatImageRenderer = ({ const cheerAmount = Number(match.replace(/\D/g, '')); return ( - <> + {match} {cheerAmount} - + ); } From 55090b1d326b9e89c15c03e983fa655bce2236c3 Mon Sep 17 00:00:00 2001 From: mjfwebb Date: Thu, 25 Sep 2025 17:04:56 +0200 Subject: [PATCH 2/2] fix: correctly load cheers from store --- src/handlers/twitch/helix/fetchCheers.ts | 2 +- src/types/twitchEvents.ts | 2 +- src/types/types.ts | 6 +- src/views/Chat/ChatImageRenderer.tsx | 84 +++++++++------------ src/views/Main/ChatPreview/ChatPreview.tsx | 88 +++++++++++++++++++--- 5 files changed, 119 insertions(+), 63 deletions(-) diff --git a/src/handlers/twitch/helix/fetchCheers.ts b/src/handlers/twitch/helix/fetchCheers.ts index f48316f..e689e20 100644 --- a/src/handlers/twitch/helix/fetchCheers.ts +++ b/src/handlers/twitch/helix/fetchCheers.ts @@ -47,7 +47,7 @@ const saveCheers = async (cheers: Cheermote[]) => { cheer.tiers.forEach((tier) => { const name = `${cheer.prefix}${tier.min_bits}`; chatCheers[name] = { - name, + prefix: cheer.prefix, color: tier.color, url: tier.images.dark.animated['4'], minBits: tier.min_bits, diff --git a/src/types/twitchEvents.ts b/src/types/twitchEvents.ts index ac68e90..37cb3d0 100644 --- a/src/types/twitchEvents.ts +++ b/src/types/twitchEvents.ts @@ -30,7 +30,7 @@ interface Emote { id: string; // An ID that uniquely identifies this emote. emote_set_id: string; // An ID that identifies the emote set that the emote belongs to. owner_id: string; // The ID of the broadcaster who owns the emote. - format: 'animated' | 'static'; // The formats that the emote is available in. For example, if the emote is available only as a static PNG, the array contains only static. But if the emote is available as a static PNG and an animated GIF, the array contains static and animated. The possible formats are: animated - An animated GIF is available for this emote. static - A static PNG file is available for this emote. + format: 'animated' | 'static'[]; // The formats that the emote is available in. For example, if the emote is available only as a static PNG, the array contains only static. But if the emote is available as a static PNG and an animated GIF, the array contains static and animated. The possible formats are: animated - An animated GIF is available for this emote. static - A static PNG file is available for this emote. } interface Mention { diff --git a/src/types/types.ts b/src/types/types.ts index b4d8787..474e425 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -17,8 +17,12 @@ export type ChatBadge = { }; export type ChatCheer = { - name: string; + prefix: string; url: string; color: string; minBits: number; }; + +export type ChatCheerWithBits = Exclude & { + bits: number; +}; diff --git a/src/views/Chat/ChatImageRenderer.tsx b/src/views/Chat/ChatImageRenderer.tsx index 2b37f24..a382081 100644 --- a/src/views/Chat/ChatImageRenderer.tsx +++ b/src/views/Chat/ChatImageRenderer.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { Fragment, JSX } from 'react'; import { store } from '../../store/store'; import { ChannelChatMessageEvent } from '../../types/twitchEvents'; -import { ChatCheer, ChatEmote } from '../../types/types'; +import { ChatCheer, ChatCheerWithBits, ChatEmote } from '../../types/types'; import { bttvModifierMap, bttvModifiers } from './bttvModifierFlags'; import { parseFrankerFaceZModifierFlags } from './parseFrankerFaceZModifierFlags'; import { parseSevenTVModifierFlags } from './parseSevenTVModifierFlags'; @@ -27,20 +27,25 @@ function getTwitchEmote(emoteId: string): ChatEmote { }; } -export const ChatImageRenderer = ({ - fragments, - bits, -}: { - fragments: ChannelChatMessageEvent['message']['fragments']; - bits?: number; -}): JSX.Element => { +export const ChatImageRenderer = ({ fragments }: { fragments: ChannelChatMessageEvent['message']['fragments'] }): JSX.Element => { const chatEmotes = store((s) => s.chatEmotes); const chatCheers = store((s) => s.chatCheers); + function findChatCheer(prefix: string, bits: number): ChatCheer | undefined { + let foundCheer: ChatCheer | undefined = undefined; + for (const cheer of Object.values(chatCheers)) { + if (cheer.prefix.toLowerCase() === prefix.toLowerCase() && cheer.minBits <= bits) { + foundCheer = cheer; + } + } + + return foundCheer; + } + const messageParts: { match: string; emote: ChatEmote | undefined; - cheer: ChatCheer | undefined; + cheer: ChatCheerWithBits | undefined; skip: boolean; modifierFlags?: string[]; }[] = []; @@ -48,6 +53,27 @@ export const ChatImageRenderer = ({ const nextMessageModifierFlags: string[] = []; fragments.forEach((fragment) => { + if (fragment.type === 'cheermote') { + if (fragment.cheermote) { + const foundCheer = findChatCheer(fragment.cheermote.prefix, fragment.cheermote.bits); + if (foundCheer) { + const cheer: ChatCheerWithBits = { + ...foundCheer, + bits: fragment.cheermote.bits, + }; + + messageParts.push({ + match: fragment.text, + emote: undefined, + cheer, + skip: false, + }); + nextMessageModifierFlags.length = 0; + return; + } + } + } + if (fragment.emote) { messageParts.push({ match: fragment.text, @@ -60,37 +86,6 @@ export const ChatImageRenderer = ({ } fragment.text.split(wordRegex).forEach((match) => { - if (bits) { - let closestCheer: ChatCheer | undefined = undefined; - // A match might look like VoHiYo199, but the cheer name is VoHiYo, so we need to remove the bits - const cheerName = match.replace(/\d+$/, ''); - for (const cheer of Object.values(chatCheers)) { - // Check if the cheer name matches the message part - if (!cheer.name.startsWith(cheerName)) { - continue; - } - - if (cheer.minBits <= bits) { - if (!closestCheer || cheer.minBits > closestCheer.minBits) { - closestCheer = cheer; - continue; - } - - closestCheer = cheer; - } - } - - if (closestCheer) { - messageParts.push({ - match, - emote: undefined, - cheer: closestCheer, - skip: false, - }); - return; - } - } - if (bttvModifiers.includes(match)) { messageParts.push({ match, @@ -183,14 +178,7 @@ export const ChatImageRenderer = ({ return ( - {match} + {match} {cheerAmount} diff --git a/src/views/Main/ChatPreview/ChatPreview.tsx b/src/views/Main/ChatPreview/ChatPreview.tsx index 4afe82e..adf6570 100644 --- a/src/views/Main/ChatPreview/ChatPreview.tsx +++ b/src/views/Main/ChatPreview/ChatPreview.tsx @@ -6,12 +6,80 @@ import { ChatEntry } from '../../Chat/ChatEntry'; import './ChatPreview.less'; -const fakesTwitchMessages = [ - 'Wow so awesome!', - 'This is so cool peepoWow', - 'Did you see that? Unbeleafable! haHAA', - 'Loving the stream, keep it up!', - '!hype Can we get some hype in the chat? catJAM', +const fakeTwitchMessages: ChannelChatMessageEvent['message'][] = [ + { + text: 'Kappa5000 keep being awesome!', + fragments: [ + { + type: 'cheermote', + text: 'Kappa5000', + cheermote: { + prefix: 'kappa', + bits: 5000, + tier: 1, + }, + }, + { + type: 'text', + text: ' keep being awesome!', + }, + ], + }, + { + text: 'This is so cool peepoWow', + fragments: [ + { + type: 'text', + text: 'This is so cool peepoWow', + }, + ], + }, + { + text: 'Did you see that? Unbeleafable! haHAA LUL', + fragments: [ + { + type: 'text', + text: 'Did you see that? Unbeleafable! haHAA ', + }, + { + type: 'emote', + text: 'LUL', + emote: { + id: '425618', + emote_set_id: '0', + owner_id: '0', + format: ['static'], + }, + }, + ], + }, + { + text: 'Loving the stream, keep it up! Cheer169', + fragments: [ + { + type: 'text', + text: 'Loving the stream, keep it up!', + }, + { + type: 'cheermote', + text: 'Cheer169', + cheermote: { + prefix: 'cheer', + bits: 169, + tier: 1, + }, + }, + ], + }, + { + text: '!hype Can we get some hype in the chat? catJAM', + fragments: [ + { + type: 'text', + text: '!hype Can we get some hype in the chat? catJAM', + }, + ], + }, ]; const fakeUsers: { @@ -85,11 +153,7 @@ export const ChatPreview = ({ overlayParameters }: { overlayParameters: typeof D const fakeChatMessageEvents = Array.from({ length: 5 }, (_, idx: number) => { { const user = fakeUsers[idx]; - const message = fakesTwitchMessages[idx]; - const chatMessage: ChannelChatMessageEvent['message'] = { - text: message, - fragments: [{ text: message, type: 'text' }], - }; + return { broadcaster_user_id: '0', broadcaster_user_login: 'athano', @@ -98,7 +162,7 @@ export const ChatPreview = ({ overlayParameters }: { overlayParameters: typeof D chatter_user_login: user.login, chatter_user_name: user.name, message_id: String(idx), - message: chatMessage, + message: fakeTwitchMessages[idx], message_type: 'text', badges: fakeUsers[idx].badges, color: user.color,