From 778e990b989cdad6376c3e7abfc958952162d73b Mon Sep 17 00:00:00 2001 From: Oliver Rose Date: Sat, 17 Oct 2026 23:28:56 -0400 Subject: [PATCH] refactor(channel): add channel badge and cheermote managers --- src/lib/commands/built-in/reload-badges.ts | 2 +- .../commands/built-in/reload-cheermotes.ts | 2 +- src/lib/managers/channel-badge-manager.ts | 42 ++++++++++++ src/lib/managers/cheermote-manager.ts | 42 ++++++++++++ src/lib/models/channel.svelte.ts | 66 +++---------------- src/lib/models/message/parse.ts | 5 +- src/lib/models/message/user-message.svelte.ts | 2 +- 7 files changed, 99 insertions(+), 62 deletions(-) create mode 100644 src/lib/managers/channel-badge-manager.ts create mode 100644 src/lib/managers/cheermote-manager.ts diff --git a/src/lib/commands/built-in/reload-badges.ts b/src/lib/commands/built-in/reload-badges.ts index a3acf21c..08f037c5 100644 --- a/src/lib/commands/built-in/reload-badges.ts +++ b/src/lib/commands/built-in/reload-badges.ts @@ -14,7 +14,7 @@ export default defineCommand({ if (providers.includes("all")) { await app.badges.fetch(true); } else { - const promises: Promise[] = [channel.fetchBadges(true)]; + const promises: Promise[] = [channel.badges.fetch(true)]; if (providers.includes("twitch")) { promises.push(app.badges.fetchTwitch(true)); diff --git a/src/lib/commands/built-in/reload-cheermotes.ts b/src/lib/commands/built-in/reload-cheermotes.ts index b0828997..2be97927 100644 --- a/src/lib/commands/built-in/reload-cheermotes.ts +++ b/src/lib/commands/built-in/reload-cheermotes.ts @@ -5,7 +5,7 @@ export default defineCommand({ name: "reload-cheermotes", description: "Reload cheermotes for the channel", async exec(_, channel) { - await channel.fetchCheermotes(true); + await channel.cheermotes.fetch(true); channel.chat.notice("Reloaded cheermotes."); }, diff --git a/src/lib/managers/channel-badge-manager.ts b/src/lib/managers/channel-badge-manager.ts new file mode 100644 index 00000000..543cadbd --- /dev/null +++ b/src/lib/managers/channel-badge-manager.ts @@ -0,0 +1,42 @@ +import * as cache from "tauri-plugin-cache-api"; + +import type { Channel } from "#lib/models/channel.svelte.js"; + +import { channelBadgesQuery } from "#lib/graphql/twitch.js"; +import { Badge } from "#lib/models/badge.js"; + +export class ChannelBadgeManager extends Map { + public constructor(public readonly channel: Channel) { + super(); + } + + /** + * Retrieves the list of badges in the channel and caches them for later + * use. + */ + public async fetch(force = false) { + if (!force && this.size) { + return this; + } + + let badges = await cache.get(`badges:${this.channel.id}`); + + if (force || !badges) { + if (force) this.clear(); + + const { user } = await this.channel.client.gql(channelBadgesQuery, { + id: this.channel.id, + }); + + badges = user?.broadcastBadges?.flatMap((b) => (b ? [Badge.fromGql(b)] : [])) ?? []; + + await cache.set(`badges:${this.channel.id}`, badges); + } + + for (const badge of badges) { + this.set(badge.id, badge); + } + + return this; + } +} diff --git a/src/lib/managers/cheermote-manager.ts b/src/lib/managers/cheermote-manager.ts new file mode 100644 index 00000000..068d35c9 --- /dev/null +++ b/src/lib/managers/cheermote-manager.ts @@ -0,0 +1,42 @@ +import * as cache from "tauri-plugin-cache-api"; + +import type { Cheermote } from "#lib/graphql/twitch.js"; +import type { Channel } from "#lib/models/channel.svelte.js"; + +import { cheermoteQuery } from "#lib/graphql/twitch.js"; + +/** + * The cheermotes in a channel keyed by their lowercased prefix. + */ +export class CheermoteManager extends Map { + public constructor(public readonly channel: Channel) { + super(); + } + + /** + * Retrieves the list of cheermotes in the channel and caches them for later + * use. + */ + public async fetch(force = false) { + if (!force && this.size) { + return this; + } + + let cheermotes = await cache.get(`cheermotes:${this.channel.id}`); + + if (force || !cheermotes) { + if (force) this.clear(); + + const { user } = await this.channel.client.gql(cheermoteQuery, { id: this.channel.id }); + + cheermotes = user?.cheer?.emotes.filter((e) => e != null) ?? []; + await cache.set(`cheermotes:${this.channel.id}`, cheermotes); + } + + for (const cheermote of cheermotes) { + this.set(cheermote.prefix.toLowerCase(), cheermote); + } + + return this; + } +} diff --git a/src/lib/models/channel.svelte.ts b/src/lib/models/channel.svelte.ts index f4462e48..b69b1fef 100644 --- a/src/lib/models/channel.svelte.ts +++ b/src/lib/models/channel.svelte.ts @@ -1,14 +1,11 @@ import { invoke } from "@tauri-apps/api/core"; -import * as cache from "tauri-plugin-cache-api"; -import type { Cheermote, PredictionOutcome } from "#lib/graphql/twitch.js"; +import type { PredictionOutcome } from "#lib/graphql/twitch.js"; import type { TwitchClient } from "#lib/twitch/client.js"; import type { IrcMessage } from "#lib/twitch/irc.js"; import { app } from "#lib/app.svelte.js"; import { - channelBadgesQuery, - cheermoteQuery, pollQuery, predictionQuery, streamQuery, @@ -24,14 +21,15 @@ import { } from "#lib/graphql/twitch.js"; import { handlers } from "#lib/handlers/index.js"; import { log } from "#lib/log.js"; +import { ChannelBadgeManager } from "#lib/managers/channel-badge-manager.js"; import { ChannelEmoteManager } from "#lib/managers/channel-emote-manager.js"; +import { CheermoteManager } from "#lib/managers/cheermote-manager.js"; import { ViewerManager } from "#lib/managers/viewer-manager.js"; import { settings } from "#lib/settings/index.js"; import { storage } from "#lib/stores.js"; import type { User } from "./user.svelte"; -import { Badge } from "./badge"; import { Chat } from "./chat.svelte"; import { Poll } from "./poll.svelte"; import { Prediction } from "./prediction.svelte"; @@ -64,7 +62,7 @@ export class Channel { /** * The badges in the channel. */ - public readonly badges = new Map(); + public readonly badges: ChannelBadgeManager; /** * The emotes in the channel. @@ -74,7 +72,7 @@ export class Channel { /** * The cheermotes in the channel. */ - public readonly cheermotes = $state([]); + public readonly cheermotes: CheermoteManager; /** * The viewers in the channel. @@ -129,7 +127,9 @@ export class Channel { this.stream = stream; this.chat = new Chat(this); + this.badges = new ChannelBadgeManager(this); this.emotes = new ChannelEmoteManager(this); + this.cheermotes = new CheermoteManager(this); this.viewers = new ViewerManager(this); this.pinned = $derived(storage.state.pinned.includes(this.id)); @@ -160,8 +160,8 @@ export class Channel { await Promise.all([ this.fetchStream(), this.emotes.fetch(), - this.fetchBadges(), - this.fetchCheermotes(), + this.badges.fetch(), + this.cheermotes.fetch(), this.chat.fetchPinned(), this.fetchPoll(), this.fetchPrediction(), @@ -226,54 +226,8 @@ export class Channel { this.chat.reset(); this.badges.clear(); this.emotes.clear(); + this.cheermotes.clear(); this.viewers.clear(); - this.cheermotes.length = 0; - } - - /** - * Retrieves the list of badges in the channel and caches them for later use. - */ - public async fetchBadges(force = false) { - if (!force && this.badges.size) { - return this.badges; - } - - let badges = await cache.get(`badges:${this.id}`); - - if (force || !badges) { - if (force) this.badges.clear(); - - const { user } = await this.client.gql(channelBadgesQuery, { id: this.id }); - badges = user?.broadcastBadges?.flatMap((b) => (b ? [Badge.fromGql(b)] : [])) ?? []; - - await cache.set(`badges:${this.id}`, badges); - } - - for (const badge of badges) { - this.badges.set(badge.id, badge); - } - - return this.badges; - } - - /** - * Retrieves the list of cheermotes in the channel and caches them for later - * use. - */ - public async fetchCheermotes(force = false) { - let cheermotes = await cache.get(`cheermotes:${this.id}`); - - if (force || !cheermotes) { - if (force) this.cheermotes.length = 0; - - const { user } = await this.client.gql(cheermoteQuery, { id: this.id }); - - cheermotes = user?.cheer?.emotes.filter((e) => e != null) ?? []; - await cache.set(`cheermotes:${this.id}`, cheermotes); - } - - this.cheermotes.push(...cheermotes); - return this.cheermotes; } /** diff --git a/src/lib/models/message/parse.ts b/src/lib/models/message/parse.ts index 8d409a1d..e83ca832 100644 --- a/src/lib/models/message/parse.ts +++ b/src/lib/models/message/parse.ts @@ -123,9 +123,8 @@ function classify(base: BaseNode, message: UserMessage, ircEmotes: IrcEmote[]): }; } - const cheermote = message.channel.cheermotes.find( - (c) => part.toLowerCase().startsWith(c.prefix.toLowerCase()) && /\d+$/.test(part), - ); + const cheer = /^(.+?)\d+$/.exec(part); + const cheermote = cheer && message.channel.cheermotes.get(cheer[1].toLowerCase()); if (cheermote) { const amount = Number(part.slice(cheermote.prefix.length)); diff --git a/src/lib/models/message/user-message.svelte.ts b/src/lib/models/message/user-message.svelte.ts index 60e00dbb..0cda14e9 100644 --- a/src/lib/models/message/user-message.svelte.ts +++ b/src/lib/models/message/user-message.svelte.ts @@ -295,7 +295,7 @@ export class UserMessage extends TextualMessage { } this.source = await app.channels.fetch(source.channel_id); - await this.source.fetchBadges(); + await this.source.badges.fetch(); this.#populateBadges(); }