Add Block Typing To SilentTyping

This commit is contained in:
thororen1234 2024-09-01 20:38:00 -04:00
parent a27c1cc163
commit 9cc0531ae1
8 changed files with 350 additions and 405 deletions

View file

@ -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

View file

@ -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<HTMLInputElement>;
onChange: (e: SyntheticEvent<HTMLInputElement>) => 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<void>; }) {
const fileInputRef = useRef<HTMLInputElement>(null);
const sound = useRef<SoundPlayer | null>(null);
const update = useForceUpdater();
return (
<Card className={cl("card")}>
<Switch
value={override.enabled}
onChange={value => {
override.enabled = value;
onChange();
update();
}}
className={Margins.bottom16}
hideBorder={true}
>
{type.name} <span className={cl("id")}>({type.id})</span>
</Switch>
<Button
color={Button.Colors.PRIMARY}
className={Margins.bottom16}
onClick={() => {
if (sound.current != null)
sound.current.stop();
sound.current = playSound(type.id);
}}
disabled={!override.enabled}
>
Preview
</Button>
<Forms.FormTitle>Replacement Sound</Forms.FormTitle>
<Button
color={Button.Colors.PRIMARY}
disabled={!override.enabled}
className={classes(Margins.right8, Margins.bottom16, cl("upload"))}
>
Upload
<FileInput
ref={fileInputRef}
onChange={event => {
event.stopPropagation();
event.preventDefault();
if (!event.currentTarget?.files?.length)
return;
const { files } = event.currentTarget;
const file = files[0];
// Set override URL to a data URI
const reader = new FileReader;
reader.onload = () => {
override.url = reader.result as string;
onChange();
update();
};
reader.readAsDataURL(file);
}}
// Sorry .caf lovers, https://en.wikipedia.org/wiki/HTML5_audio#Supported_audio_coding_formats
filters={[{ extensions: ["mp3", "wav", "ogg", "webm", "flac"] }]}
/>
</Button>
<Button
color={Button.Colors.RED}
onClick={() => {
override.url = "";
onChange();
update();
}}
disabled={!(override.enabled && override.url.length !== 0)}
style={{ display: "inline" }}
className={classes(Margins.right8, Margins.bottom16)}
>
Clear
</Button>
<Forms.FormTitle>Volume</Forms.FormTitle>
<Slider
markers={makeRange(0, 100, 10)}
initialValue={override.volume}
onValueChange={value => {
override.volume = value;
onChange();
update();
}}
className={Margins.bottom16}
disabled={!override.enabled}
/>
</Card>
);
}

View file

@ -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)}"`
}
}))
)
});

View file

@ -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<string, SoundOverride> = soundTypes.reduce((result, sound) => ({ ...result, [sound.id]: makeEmptyOverride() }), {});
const settings = definePluginSettings({
overrides: {
type: OptionType.COMPONENT,
description: "",
component: () =>
<>
{soundTypes.map(type =>
<SoundOverrideComponent
key={type.id}
type={type}
override={overrides[type.id]}
onChange={() => 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();
}
});

View file

@ -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;
}

View file

@ -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
};
}

View file

@ -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"),
});

View file

@ -779,6 +779,10 @@ export const EquicordDevs = Object.freeze({
name: "vMohammad",
id: 921098159348924457n
},
SpikeHD: {
name: "SpikeHD",
id: 221757857836564485n
}
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly