From 2e50395856b17ad9e0ef86a25e95545d8b72f217 Mon Sep 17 00:00:00 2001 From: BennyyyBB Date: Sun, 21 Jun 2026 17:35:04 +0200 Subject: [PATCH 1/9] add per-channel emote blacklist --- src/app/chat/UserMessage.vue | 7 ++++++ src/common/chat/Tokenizer.ts | 4 +++- src/composable/chat/useEmoteBlacklist.ts | 27 ++++++++++++++++++++++++ src/site/global/GlobalSettings.ts | 5 +++++ src/site/global/components/EmoteCard.vue | 10 +++++++++ 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/composable/chat/useEmoteBlacklist.ts diff --git a/src/app/chat/UserMessage.vue b/src/app/chat/UserMessage.vue index d2e468121..34573b348 100644 --- a/src/app/chat/UserMessage.vue +++ b/src/app/chat/UserMessage.vue @@ -144,6 +144,10 @@ const { openViewerCard } = useChatTools(ctx); const { pinChatMessage } = useChatModeration(ctx, msg.value.author?.username ?? ""); const emoteScale = useConfig("chat.emote_scale"); +const blacklistMap = useConfig>("chat.emote_blacklist_per_channel"); +const hiddenEmotes = computed( + () => new Set(blacklistMap.value?.[ctx.username?.toLowerCase()] ?? []), +); // TODO: css variables const meStyle = useConfig("chat.slash_me_style"); @@ -174,6 +178,7 @@ function doTokenize() { emoteMap: props.emotes ?? {}, localEmoteMap: { ...cosmetics.emotes, ...props.msg.nativeEmotes }, showModifiers: showModifiers.value, + hiddenEmotes: hiddenEmotes.value, }); const result: MessageTokenOrText[] = []; @@ -202,6 +207,8 @@ function doTokenize() { tokens.value = result; } +watch(hiddenEmotes, () => doTokenize()); + function doPinMessage(): void { pinChatMessage(msg.value.id, 1200)?.catch((err) => log.error("failed to pin chat message", err)); } diff --git a/src/common/chat/Tokenizer.ts b/src/common/chat/Tokenizer.ts index 4ec56c7dd..7b37d8753 100644 --- a/src/common/chat/Tokenizer.ts +++ b/src/common/chat/Tokenizer.ts @@ -24,10 +24,11 @@ export class Tokenizer { const textParts = this.msg.body.split(" "); const getEmote = (name: string) => { + if (!name) return; + if (opt.hiddenEmotes?.has(name)) return; // treat as plaint text if (opt.localEmoteMap?.[name] && Object.hasOwn(opt.localEmoteMap, name)) { return opt.localEmoteMap[name]; } - if (opt.emoteMap[name] && Object.hasOwn(opt.emoteMap, name)) { return opt.emoteMap[name]; } @@ -158,4 +159,5 @@ export interface TokenizeOptions { filteredWords?: string[]; actorUsername?: string; showModifiers?: boolean; + hiddenEmotes?: Set; // emote names to render as plain text } diff --git a/src/composable/chat/useEmoteBlacklist.ts b/src/composable/chat/useEmoteBlacklist.ts new file mode 100644 index 000000000..ec136fa26 --- /dev/null +++ b/src/composable/chat/useEmoteBlacklist.ts @@ -0,0 +1,27 @@ +import { computed } from "vue"; +import { useConfig } from "@/composable/useSettings"; +import { useChannelContext } from "@/composable/channel/useChannelContext"; + +export function useEmoteBlacklist() { + const ctx = useChannelContext(); + const map = useConfig>("chat.emote_blacklist_per_channel"); + + const key = computed(() => (ctx.username || ctx.id || "").toLowerCase()); + + const list = computed(() => map.value?.[key.value] ?? []); + + function isHidden(name: string) { + return list.value.includes(name); + } + + function toggle(name: string) { + const current = { ...(map.value ?? {}) }; + const arr = new Set(current[key.value] ?? []); + if (arr.has(name)) arr.delete(name); + else arr.add(name); + current[key.value] = [...arr]; + map.value = current; + } + + return { list, isHidden, toggle }; +} diff --git a/src/site/global/GlobalSettings.ts b/src/site/global/GlobalSettings.ts index 0953380f5..1c413d032 100644 --- a/src/site/global/GlobalSettings.ts +++ b/src/site/global/GlobalSettings.ts @@ -25,6 +25,11 @@ export const dataSettings = [ defaultValue: new Set(), serialize: false, }), + declareConfig("chat.emote_blacklist_per_channel", "NONE", { + label: "Per-channel hidden 7TV emotes", // Map + defaultValue: {} as Record, + serialize: true, + }), ]; export const globalSettings = [ diff --git a/src/site/global/components/EmoteCard.vue b/src/site/global/components/EmoteCard.vue index 2b8d7acec..7f6137f54 100644 --- a/src/site/global/components/EmoteCard.vue +++ b/src/site/global/components/EmoteCard.vue @@ -56,6 +56,10 @@ import { userQuery } from "@/assets/gql/seventv.user.gql"; import { emoteCardQuery } from "@/assets/gql/tw.emote-card.gql"; import OpenLinkIcon from "@/assets/svg/icons/OpenLinkIcon.vue"; import { useQuery } from "@vue/apollo-composable"; +import { useEmoteBlacklist } from "@/composable/chat/useEmoteBlacklist"; + +const { isHidden, toggle } = useEmoteBlacklist(); +defineProps<{ emote: SevenTV.ActiveEmote }>(); const props = defineProps<{ emote: SevenTV.ActiveEmote; @@ -187,6 +191,12 @@ watch( ); + + diff --git a/src/site/global/GlobalSettings.ts b/src/site/global/GlobalSettings.ts index 1c413d032..95913afd2 100644 --- a/src/site/global/GlobalSettings.ts +++ b/src/site/global/GlobalSettings.ts @@ -30,6 +30,12 @@ export const dataSettings = [ defaultValue: {} as Record, serialize: true, }), + declareConfig("chat.emote_blacklist_per_channel.enabled", "TOGGLE", { + label: "Per-channel emote blacklist", + hint: "Replace blacklisted 7TV emotes with plain text in the channel they were hidden in", + path: ["Chat", "Emotes"], + defaultValue: true, + }), ]; export const globalSettings = [ From 280a704ad2c9dfc7d054b23d05f2fa1e9f4b8103 Mon Sep 17 00:00:00 2001 From: BennyyyBB Date: Sun, 21 Jun 2026 20:52:53 +0200 Subject: [PATCH 4/9] formatted prettier --- src/app/chat/UserMessage.vue | 4 +-- src/app/settings/Settings.ts | 2 +- .../settings/SettingsViewEmoteBlacklist.vue | 12 +++---- src/composable/chat/useEmoteBlacklist.ts | 34 +++++++++---------- src/site/global/components/EmoteCard.vue | 9 +++-- .../modules/chat/ChatController.vue | 32 +++++++++-------- 6 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/app/chat/UserMessage.vue b/src/app/chat/UserMessage.vue index 34573b348..9bba48b64 100644 --- a/src/app/chat/UserMessage.vue +++ b/src/app/chat/UserMessage.vue @@ -145,9 +145,7 @@ const { pinChatMessage } = useChatModeration(ctx, msg.value.author?.username ?? const emoteScale = useConfig("chat.emote_scale"); const blacklistMap = useConfig>("chat.emote_blacklist_per_channel"); -const hiddenEmotes = computed( - () => new Set(blacklistMap.value?.[ctx.username?.toLowerCase()] ?? []), -); +const hiddenEmotes = computed(() => new Set(blacklistMap.value?.[ctx.username?.toLowerCase()] ?? [])); // TODO: css variables const meStyle = useConfig("chat.slash_me_style"); diff --git a/src/app/settings/Settings.ts b/src/app/settings/Settings.ts index 5f3a12086..61d563f09 100644 --- a/src/app/settings/Settings.ts +++ b/src/app/settings/Settings.ts @@ -3,9 +3,9 @@ import { LOCAL_STORAGE_KEYS } from "@/common/Constant"; import SettingsViewBackupVue from "./SettingsViewBackup.vue"; import SettingsViewCompatVue from "./SettingsViewCompat.vue"; import SettingsViewConfigVue from "./SettingsViewConfig.vue"; +import SettingsViewEmoteBlacklistVue from "./SettingsViewEmoteBlacklist.vue"; import SettingsViewHomeVue from "./SettingsViewHome.vue"; import SettingsViewProfileVue from "./SettingsViewProfile.vue"; -import SettingsViewEmoteBlacklistVue from "./SettingsViewEmoteBlacklist.vue"; const PaintTool = defineAsyncComponent(() => import("@/app/paint-tool/PaintTool.vue")); const Store = defineAsyncComponent(() => import("@/app/store/Store.vue")); diff --git a/src/app/settings/SettingsViewEmoteBlacklist.vue b/src/app/settings/SettingsViewEmoteBlacklist.vue index 0e59ea9e9..143fc395e 100644 --- a/src/app/settings/SettingsViewEmoteBlacklist.vue +++ b/src/app/settings/SettingsViewEmoteBlacklist.vue @@ -2,8 +2,8 @@

Per-Channel Emote Blacklist

- Hide individual 7TV emotes in specific channels. The message stays readable — only the emote - image is replaced by its plain text. + Hide individual 7TV emotes in specific channels. The message stays readable — only the emote image is + replaced by its plain text.

@@ -42,8 +42,8 @@