diff --git a/README.md b/README.md index bf69c574..68a542ee 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - ColorMessage by Kyuuhachi - CommandPalette by Ethan - CopyUserMention by Cortex and castdrian -- CustomSounds by ScattrdBlade +- CustomSounds by TheKodeToad and SpikeHD - CuteAnimeBoys by ShadyGoat - CuteNekos by echo - CutePats by thororen diff --git a/src/equicordplugins/customSounds/components/SoundOverrideComponent.tsx b/src/equicordplugins/customSounds/components/SoundOverrideComponent.tsx new file mode 100644 index 00000000..4ed903f3 --- /dev/null +++ b/src/equicordplugins/customSounds/components/SoundOverrideComponent.tsx @@ -0,0 +1,119 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { makeRange } from "@components/PluginSettings/components"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { useForceUpdater } from "@utils/react"; +import { findByCodeLazy, findLazy } from "@webpack"; +import { Button, Card, Forms, Slider, Switch, useRef } from "@webpack/common"; +import { ComponentType, Ref, SyntheticEvent } from "react"; + +import { SoundOverride, SoundPlayer, SoundType } from "../types"; + +type FileInput = ComponentType<{ + ref: Ref; + onChange: (e: SyntheticEvent) => void; + multiple?: boolean; + filters?: { name?: string; extensions: string[]; }[]; +}>; + +const playSound: (id: string) => SoundPlayer = findByCodeLazy(".playWithListener().then"); +const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef); +const cl = classNameFactory("vc-custom-sounds-"); + +export function SoundOverrideComponent({ type, override, onChange }: { type: SoundType; override: SoundOverride; onChange: () => Promise; }) { + const fileInputRef = useRef(null); + const sound = useRef(null); + const update = useForceUpdater(); + + return ( + + { + override.enabled = value; + onChange(); + update(); + }} + className={Margins.bottom16} + hideBorder={true} + > + {type.name} ({type.id}) + + + Replacement Sound + + + Volume + { + override.volume = value; + onChange(); + update(); + }} + className={Margins.bottom16} + disabled={!override.enabled} + /> + + ); +} diff --git a/src/equicordplugins/customSounds/index.ts b/src/equicordplugins/customSounds/index.ts deleted file mode 100644 index cea81ce1..00000000 --- a/src/equicordplugins/customSounds/index.ts +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const soundFileMapping: { [key: string]: string[]; } = { - discodoDetuneURL: ["c9bfe03395cf2616891f.mp3", "9b8b7e8c94287d5491a8.mp3"], - activitiesRocketTimeURL: ["cd402df20cddf4d85b4b.mp3"], - activityEndURL: ["37530244cdcd5141095b.mp3"], - activityLaunchURL: ["267f72c6f838aac3be94.mp3"], - activityUserJoinURL: ["74c606872cea9803e310.mp3"], - activityUserLeftURL: ["99bd2585703114d2df64.mp3"], - asmrMessage1URL: ["d04d1ee13ab2d7d04e97.mp3"], - bitMessage1URL: ["fd9f21c60424f7bbe603.mp3"], - bopMessage1URL: ["f9b3c218d2bac00a50a5.mp3"], - callCallingURL: ["11b68eb8f243b5f6c8d7.mp3", "ec09898a0bd65dfaa768.mp3"], - callRingingURL: ["986703daecf955ce3ce3.mp3", "6345bccfecdfa67fdb97.mp3"], - callRingingBeatURL: ["3b3a2f5f29b9cb656efb.mp3"], - callRingingHalloweenURL: ["feb12b25f1200b97c4eb.mp3"], - callRingingSnowHalationURL: ["99b1d8a6fe0b95e99827.mp3"], - callRingingSnowsgivingURL: ["54527e70cf0ddaeff76f.mp3"], - clipErrorURL: ["4185e05ac87668c95db7.mp3"], - clipSaveURL: ["f96b272b4140be6ce8a9.mp3"], - ddrDownURL: ["60b2fa578027733f07b2.mp3"], - ddrLeftURL: ["6a2283291b8468b5dcbc.mp3"], - ddrRightURL: ["ede3b86253bb4aa1615b.mp3"], - ddrUpURL: ["89547833e1e1ebb138a4.mp3"], - deafenURL: ["763976e8bc7c745f1bbb.mp3", "1e63dc2f54bef5c5003c.mp3"], - disconnectDetuneURL: ["752b32be8f41b5ef55c4.mp3"], - duckyMessage1URL: ["7732a4c7760789c64269.mp3"], - hangStatusSelectURL: ["6f82a1ced41ffba7e474.mp3"], - highfiveClapURL: ["b6765c41e5305ed3ccbf.mp3"], - highfiveWhistleURL: ["ea499ca7cb948b4e89f3.mp3"], - humanManURL: ["ba335c8e058cf0edef0c.mp3"], - lofiMessage1URL: ["8fd01840800c5b8d5d40.mp3"], - mention1URL: ["72d6195de7af6f6c522f.mp3"], - mention2URL: ["5746c97822ddd998ecaa.mp3"], - mention3URL: ["90268f54ea2962dd9a9d.mp3"], - message1DetuneURL: ["70ae9d5bc54e4e0954f9.mp3", "ed256a76f3fe748177de.mp3"], - message2URL: ["94d97da9d3ca65ca5c48.mp3"], - message3URL: ["647a0cfe7a004fa8b20d.mp3"], - muteDetuneURL: ["ad1365b07daf20cf62d5.mp3", "e8ffe6892e47d655e796.mp3"], - overlayunlockURL: ["cf5424f20c2a9c65b6bc.mp3"], - poggermodeAchievementUnlockURL: ["156fff4a60f8bd215dd9.mp3"], - poggermodeApplauseURL: ["07ff9c560f9ba1c99cc8.mp3"], - poggermodeEnabledURL: ["73eaa6460d4bdb9dcd4d.mp3"], - poggermodeMessageSendURL: ["7e01b5cb50d9d1941f16.mp3"], - pttStartDetuneURL: ["369f4aaf35687b9f986d.mp3", "3d1f7316482d3b37e947.mp3"], - pttStopDetuneURL: ["b843a42563e5f3144483.mp3", "1e1af3535e94d2143b69.mp3"], - reconnectURL: ["da2dc3239ecd9ab74be1.mp3"], - robotManURL: ["76ff191944dd58f4835f.mp3"], - stageWaitingURL: ["e23b4d3cf753989097f4.mp3"], - streamEndedDetuneURL: ["9b12ba39365fff66dd95.mp3", "8f4bc39481c815b02e4c.mp3"], - streamStartedDetuneURL: ["d954d7bfa51d58a25610.mp3", "7868133f107297d09719.mp3"], - streamUserJoinedDetuneURL: ["e9fa25653b507623acbd.mp3", "5320b9eae726f7c7b3f5.mp3"], - streamUserLeftDetuneURL: ["0fdb31ebfdaf7e86e7ff.mp3", "b45e91a78a4377b853b8.mp3"], - successURL: ["1cc608397adb2bb151de.mp3"], - undeafenDetuneURL: ["2f7089b0d3e66e7d6d67.mp3", "c87a93fd4b6918f21b8d.mp3"], - unmuteDetuneURL: ["80f50a0752b5cd6028ac.mp3", "1bf3e7ad8588f290f1d8.mp3"], - userJoinDetuneURL: ["93034ac8d9eba50e3354.mp3", "376a8b2b79947dabac6d.mp3"], - userLeaveDetuneURL: ["fea7a918aecf33a04116.mp3", "9ea3b5ecbcad19a243e6.mp3"], - userMovedDetuneURL: ["e490f52f12a18334ae94.mp3", "af380400eb22d2fa80dc.mp3"], - vibingWumpusURL: ["38b1c58d275e828aa9b6.mp3"] -}; - -const settings = definePluginSettings({ - discodoDetuneURL: { - description: "Audio URL for discodo sound", - type: OptionType.STRING, - default: "https://www.myinstants.com/media/sounds/explosion-m.mp3", - restartNeeded: true - }, - activitiesRocketTimeURL: { - description: "Audio URL for activities rocket time sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - activityEndURL: { - description: "Audio URL for activity end sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - activityLaunchURL: { - description: "Audio URL for activity launch sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - activityUserJoinURL: { - description: "Audio URL for activity user join sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - activityUserLeftURL: { - description: "Audio URL for activity user left sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - asmrMessage1URL: { - description: "Audio URL for asmr message1 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - bitMessage1URL: { - description: "Audio URL for bit message1 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - bopMessage1URL: { - description: "Audio URL for bop message1 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - callCallingURL: { - description: "Audio URL for call calling sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - callRingingURL: { - description: "Audio URL for call ringing sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - callRingingBeatURL: { - description: "Audio URL for call ringing beat sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - callRingingHalloweenURL: { - description: "Audio URL for call ringing halloween sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - callRingingSnowHalationURL: { - description: "Audio URL for call ringing snow halation sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - callRingingSnowsgivingURL: { - description: "Audio URL for call ringing snowsgiving sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - clipErrorURL: { - description: "Audio URL for clip error sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - clipSaveURL: { - description: "Audio URL for clip save sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - ddrDownURL: { - description: "Audio URL for ddr down sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - ddrLeftURL: { - description: "Audio URL for ddr left sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - ddrRightURL: { - description: "Audio URL for ddr right sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - ddrUpURL: { - description: "Audio URL for ddr up sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - deafenURL: { - description: "Audio URL for deafen sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - disconnectDetuneURL: { - description: "Audio URL for disconnect sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - duckyMessage1URL: { - description: "Audio URL for ducky message1 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - hangStatusSelectURL: { - description: "Audio URL for hang status select sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - highfiveClapURL: { - description: "Audio URL for highfive clap sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - highfiveWhistleURL: { - description: "Audio URL for highfive whistle sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - humanManURL: { - description: "Audio URL for human man sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - lofiMessage1URL: { - description: "Audio URL for lofi message1 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - mention1URL: { - description: "Audio URL for mention1 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - mention2URL: { - description: "Audio URL for mention2 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - mention3URL: { - description: "Audio URL for mention3 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - message2URL: { - description: "Audio URL for message2 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - message3URL: { - description: "Audio URL for message3 sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - muteDetuneURL: { - description: "Audio URL for mute sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - overlayunlockURL: { - description: "Audio URL for overlay unlock sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - poggermodeAchievementUnlockURL: { - description: "Audio URL for poggermode achievement unlock sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - poggermodeApplauseURL: { - description: "Audio URL for poggermode applause sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - poggermodeEnabledURL: { - description: "Audio URL for poggermode enabled sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - poggermodeMessageSendURL: { - description: "Audio URL for poggermode message send sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - pttStartDetuneURL: { - description: "Audio URL for ptt start sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - pttStopDetuneURL: { - description: "Audio URL for ptt stop sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - reconnectURL: { - description: "Audio URL for reconnect sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - robotManURL: { - description: "Audio URL for robot man sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - stageWaitingURL: { - description: "Audio URL for stage waiting sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - successURL: { - description: "Audio URL for success sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - }, - vibingWumpusURL: { - description: "Audio URL for vibing wumpus sound", - type: OptionType.STRING, - default: "", - restartNeeded: true - } -}); - -function getSoundURL(settingKey: string) { - const url = settings.store[settingKey]; - const knownAudioExtensions = [".mp3", ".wav", ".ogg", ".flac", ".aac", ".m4a"]; - - const isValidURL = (url: string) => { - try { - new URL(url); - return true; - } catch (_) { - return false; - } - }; - - const hasValidExtension = (url: string) => { - return knownAudioExtensions.some(ext => url.toLowerCase().endsWith(ext)); - }; - - const addHttpsIfMissing = (url: string) => { - if (!/^https?:\/\//i.test(url)) { - return `https://${url}`; - } - return url; - }; - - const correctedURL = addHttpsIfMissing(url); - - if (correctedURL && isValidURL(correctedURL) && hasValidExtension(correctedURL)) { - return correctedURL; - } - return url; -} - -export default definePlugin({ - name: "CustomSounds", - description: "Replace Discord sounds with a custom ones", - authors: [Devs.ScattrdBlade], - settings, - patches: Object.keys(soundFileMapping).flatMap(settingKey => - soundFileMapping[settingKey].map(file => ({ - find: file, - replacement: { - match: /e\.exports\s*=\s*n\.p\s*\+\s*"[a-zA-Z0-9]+\.(mp3|wav|ogg|flac|aac|m4a)"/, - replace: () => `e.exports="${getSoundURL(settingKey)}"` - } - })) - ) -}); diff --git a/src/equicordplugins/customSounds/index.tsx b/src/equicordplugins/customSounds/index.tsx new file mode 100644 index 00000000..57584d77 --- /dev/null +++ b/src/equicordplugins/customSounds/index.tsx @@ -0,0 +1,89 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { DataStore } from "@api/index"; +import { definePluginSettings } from "@api/Settings"; +import { Devs, EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { React } from "@webpack/common"; + +import { SoundOverrideComponent } from "./components/SoundOverrideComponent"; +import { makeEmptyOverride, SoundOverride, soundTypes } from "./types"; + +const OVERRIDES_KEY = "CustomSounds_overrides"; +let overrides: Record = soundTypes.reduce((result, sound) => ({ ...result, [sound.id]: makeEmptyOverride() }), {}); + +const settings = definePluginSettings({ + overrides: { + type: OptionType.COMPONENT, + description: "", + component: () => + <> + {soundTypes.map(type => + DataStore.set(OVERRIDES_KEY, overrides)} + /> + )} + + } +}); + +export function isOverriden(id: string): boolean { + return overrides[id]?.enabled ?? false; +} + +export function findOverride(id: string): SoundOverride | null { + const result = overrides[id]; + if (!result?.enabled) + return null; + + return result; +} + +export default definePlugin({ + name: "CustomSounds", + description: "Replace Discord's sounds with your own.", + authors: [Devs.TheKodeToad, EquicordDevs.SpikeHD], + patches: [ + // sound class + { + find: 'Error("could not play audio")', + replacement: [ + // override URL + { + match: /(?<=new Audio;\i\.src=)\i\([0-9]+\)\("\.\/"\.concat\(this\.name,"\.mp3"\)/, + replace: "$self.findOverride(this.name)?.url || $&" + }, + // override volume + { + match: /Math.min\(\i\.\i\.getOutputVolume\(\)\/100\*this\._volume/, + replace: "$& * ($self.findOverride(this.name)?.volume ?? 100) / 100" + } + ] + }, + // force classic soundpack for overriden sounds + { + find: ".playWithListener().then", + replacement: { + match: /\i\.\i\.getSoundpack\(\)/, + replace: '$self.isOverriden(arguments[0]) ? "classic" : $&' + } + } + ], + settings, + findOverride, + isOverriden, + async start() { + overrides = await DataStore.get(OVERRIDES_KEY) ?? {}; + for (const type of soundTypes) + overrides[type.id] ??= makeEmptyOverride(); + } +}); diff --git a/src/equicordplugins/customSounds/styles.css b/src/equicordplugins/customSounds/styles.css new file mode 100644 index 00000000..84270449 --- /dev/null +++ b/src/equicordplugins/customSounds/styles.css @@ -0,0 +1,11 @@ +.vc-custom-sounds-card { + padding: 1em 1em 0; +} + +.vc-custom-sounds-id { + color: var(--text-muted); +} + +.vc-custom-sounds-upload { + display: inline; +} diff --git a/src/equicordplugins/customSounds/types.ts b/src/equicordplugins/customSounds/types.ts new file mode 100644 index 00000000..adbb270e --- /dev/null +++ b/src/equicordplugins/customSounds/types.ts @@ -0,0 +1,57 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface SoundType { + name: string; + id: string; +} + +export interface SoundOverride { + enabled: boolean; + url: string; + volume: number; +} + +export interface SoundPlayer { + loop(): void; + play(): void; + pause(): void; + stop(): void; +} + +export const soundTypes: readonly SoundType[] = [ + { name: "Message", id: "message1" }, + { name: "Message (Focused Channel)", id: "message3" }, + { name: "Defean", id: "deafen" }, + { name: "Undefean", id: "undeafen" }, + { name: "Mute", id: "mute" }, + { name: "Unmute", id: "unmute" }, + { name: "Voice Disconnected", id: "disconnect" }, + { name: "PTT Activate", id: "ptt_start" }, + { name: "PTT Deactive", id: "ptt_stop" }, + { name: "User Join", id: "user_join" }, + { name: "User Leave", id: "user_leave" }, + { name: "User Moved", id: "user_moved" }, + { name: "Outgoing Ring", id: "call_calling" }, + { name: "Incoming Ring", id: "call_ringing" }, + { name: "Stream Started", id: "stream_started" }, + { name: "Stream Ended", id: "stream_ended" }, + { name: "Viewer Join", id: "stream_user_joined" }, + { name: "Viewer Leave", id: "stream_user_left" }, + { name: "Activity Start", id: "activity_launch" }, + { name: "Activity End", id: "activity_end" }, + { name: "Activity User Join", id: "activity_user_join" }, + { name: "Activity User Leave", id: "activity_user_left" }, + { name: "Invited to Speak", id: "reconnect" } +] as const; + +export function makeEmptyOverride(): SoundOverride { + return { + enabled: false, + url: "", + volume: 100 + }; +} diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index 2a6a6428..d9f52eae 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -40,6 +40,48 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, description: "Toggle functionality", default: true, + }, + blockAllTypingIndicators: { + type: OptionType.BOOLEAN, + description: "Toggle functionality on all typing indicators on avatars", + default: false, + restartNeeded: true + }, + blockAllIsTyping: { + type: OptionType.BOOLEAN, + description: "Toggle functionality on all 'Is Typing...'", + default: false, + restartNeeded: true + }, + oldBlockAllTypingIndicators: { + type: OptionType.BOOLEAN, + description: "Toggle functionality on all 'Is Typing...'", + default: false, + restartNeeded: true, + hidden: true + }, + oldBlockAllIsTyping: { + type: OptionType.BOOLEAN, + description: "Toggle functionality on all 'Is Typing...'", + default: false, + restartNeeded: true, + hidden: true + }, + blockEverything: { + type: OptionType.BOOLEAN, + description: "Toggle functionality on Everything", + default: false, + restartNeeded: true, + onChange: (value: boolean) => { + settings.store.oldBlockAllTypingIndicators = settings.store.blockAllTypingIndicators; + settings.store.oldBlockAllIsTyping = settings.store.blockAllIsTyping; + if (!value) { + settings.store.blockAllTypingIndicators = settings.store.oldBlockAllTypingIndicators; + settings.store.blockAllIsTyping = settings.store.oldBlockAllIsTyping; + } + settings.store.blockAllTypingIndicators = value; + settings.store.blockAllIsTyping = value; + } } }); @@ -83,7 +125,6 @@ const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => { ); }; - export default definePlugin({ name: "SilentTyping", authors: [Devs.Ven, Devs.Rini, Devs.ImBanana], @@ -101,6 +142,30 @@ export default definePlugin({ replace: "startTyping:$self.startTyping,stop" } }, + { + find: "isTyping:", + all: true, + noWarn: true, + replacement: { + match: /isTyping:.+?([,}].*?\))/g, + replace: (m, rest) => { + const destructuringMatch = rest.match(/}=.+/); + if (destructuringMatch == null) return `isTyping:!1${rest}`; + return m; + } + }, + predicate: () => settings.store.blockAllTypingIndicators + }, + { + find: "getTypingUsers(", + all: true, + noWarn: true, + replacement: { + match: /getTypingUsers\(.*?\)/, + replace: "getTypingUsers()" + }, + predicate: () => settings.store.blockAllIsTyping + } ], commands: [{ @@ -128,6 +193,8 @@ export default definePlugin({ FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); }, - start: () => addChatBarButton("SilentTyping", SilentTypingToggle), + start: () => { + addChatBarButton("SilentTyping", SilentTypingToggle); + }, stop: () => removeChatBarButton("SilentTyping"), }); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 305a0023..710257a2 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -779,6 +779,10 @@ export const EquicordDevs = Object.freeze({ name: "vMohammad", id: 921098159348924457n }, + SpikeHD: { + name: "SpikeHD", + id: 221757857836564485n + } } satisfies Record); // iife so #__PURE__ works correctly