Compare commits

...

22 Commits

Author SHA1 Message Date
Vendicated
cae8b1a93b Bump to v1.1.2 2023-03-22 04:07:12 +01:00
Nuckyz
a1c1fec8cb Improve Fake Nitro client themes bypass (#654) 2023-03-22 03:01:32 +00:00
Lewis Crichton
55a66dbb39 fix(RoleColorEverywhere): MessageLinkEmbeds DM error (#648) 2023-03-21 23:57:53 -03:00
Nuckyz
a2f0c912f0 Fix Fake Nitro making Discord unusable and ColorSighted not working sometimes (#640) 2023-03-21 09:41:31 +00:00
Nuckyz
e29bbf73aa Fix Nitro Themes with FakeNitro (#639) 2023-03-21 09:03:28 +00:00
Nuckyz
0ba3e9f469 I'm sorry for hurting you Fake Nitro (#637) 2023-03-21 06:41:11 +00:00
Nuckyz
6f200e9218 Fix grammar and SHC patches matching wrong var (#636) 2023-03-21 06:30:09 +00:00
Nuckyz
586b26d2d4 Fixes and ShowHiddenChannels improvements (#634)
~ Fixes #630
~ Fixes #618
2023-03-21 06:07:16 +00:00
Ven
d482d33d6f Fix the infamous MessageClickActions bug 2023-03-21 03:19:02 +01:00
Dossy Shiobara
37c2a8a5de fix: settings input validation and error handling (#609)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-21 02:16:01 +00:00
Dossy Shiobara
265547213c docs: clarify empty patches array behavior (#610)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-21 02:14:27 +00:00
afn
87e46f5a5a chore(friendInvites): improve descriptions formatting (#628) 2023-03-21 03:13:11 +01:00
Nuckyz
e36f4e5b0a Fixes and make guild tooltip show users inside hidden voice channels (#613)
* Fix #509

* Fix #597

* Fix #594
2023-03-19 22:03:33 -03:00
Xinto
4aff11421f Replace update notices with notifications (#558) 2023-03-19 09:21:26 +00:00
Nuckyz
ea642d9e90 Fix #598 (#612) 2023-03-19 08:44:11 +00:00
fawn
17c3496542 feat(typingIndicator): Option to not show indicator for blocked users (#513) 2023-03-19 05:13:17 -03:00
Nuckyz
0fb79b763d Improvements, changes and fixes (#611) 2023-03-19 04:53:00 -03:00
whqwert
5873bde6a6 fix(apiMessagePopover): fix match (#608) 2023-03-18 22:38:08 +01:00
Nuckyz
0b79387800 feat(PlatformIndicators): Colored mobile indicator option (#536)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-18 04:58:49 +01:00
Lewis Crichton
6b493bc7d9 feat(plugin): F8Break (#581)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-18 04:54:19 +01:00
LordElias
de53bc7991 messageLogger: fix edited timestamp styling & add i18n (#607) 2023-03-18 04:37:55 +01:00
Lewis Crichton
4c5a56a8a5 fix(RoleColorEverywhere): Chat mentions (#605) 2023-03-15 22:27:46 -03:00
38 changed files with 530 additions and 179 deletions

View File

@ -26,6 +26,10 @@ export default definePlugin({
name: "Your Name", name: "Your Name",
}, },
], ],
// Delete `patches` if you are not using code patches, as it will make
// your plugin require restarts, and your stop() method will not be
// invoked at all. The presence of the key in the object alone is
// enough to trigger this behavior, even if the value is an empty array.
patches: [], patches: [],
// Delete these two below if you are only using code patches // Delete these two below if you are only using code patches
start() {}, start() {},

View File

@ -1,9 +1,9 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.1.1", "version": "1.1.2",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"keywords": [], "keywords": [ ],
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
"url": "https://github.com/Vendicated/Vencord/issues" "url": "https://github.com/Vendicated/Vencord/issues"

View File

@ -27,7 +27,7 @@ export { PlainSettings, Settings };
import "./utils/quickCss"; import "./utils/quickCss";
import "./webpack/patchWebpack"; import "./webpack/patchWebpack";
import { popNotice, showNotice } from "./api/Notices"; import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/settings"; import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins"; import { patches, PMLogger, startAllPlugins } from "./plugins";
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater"; import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
@ -49,32 +49,30 @@ async function init() {
if (Settings.autoUpdate) { if (Settings.autoUpdate) {
await update(); await update();
const needsFullRestart = await rebuild(); const needsFullRestart = await rebuild();
setTimeout(() => { if (Settings.autoUpdateNotification)
showNotice( setTimeout(() => showNotification({
"Vencord has been updated!", title: "Vencord has been updated!",
"Restart", body: "Click here to restart",
() => { permanent: true,
onClick() {
if (needsFullRestart) if (needsFullRestart)
window.DiscordNative.app.relaunch(); window.DiscordNative.app.relaunch();
else else
location.reload(); location.reload();
} }
); }), 10_000);
}, 10_000);
return; return;
} }
if (Settings.notifyAboutUpdates) if (Settings.notifyAboutUpdates)
setTimeout(() => { setTimeout(() => showNotification({
showNotice( title: "A Vencord update is available!",
"A Vencord update is available!", body: "Click here to view the update",
"View Update", permanent: true,
() => { onClick() {
popNotice(); SettingsRouter.open("VencordUpdater");
SettingsRouter.open("VencordUpdater"); }
} }), 10_000);
);
}, 10_000);
} catch (err) { } catch (err) {
UpdateLogger.error("Failed to check for updates", err); UpdateLogger.error("Failed to check for updates", err);
} }

View File

@ -23,13 +23,13 @@ import type { ReactElement } from "react";
* @param children The rendered context menu elements * @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
*/ */
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, args?: Array<any>) => void; export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => void;
/** /**
* @param The navId of the context menu being patched * @param The navId of the context menu being patched
* @param children The rendered context menu elements * @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
*/ */
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, args?: Array<any>) => void; export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => void;
const ContextMenuLogger = new Logger("ContextMenu"); const ContextMenuLogger = new Logger("ContextMenu");
@ -119,12 +119,13 @@ interface ContextMenuProps {
} }
export function _patchContextMenu(props: ContextMenuProps) { export function _patchContextMenu(props: ContextMenuProps) {
props.contextMenuApiArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId); const contextMenuPatches = navPatches.get(props.navId);
if (contextMenuPatches) { if (contextMenuPatches) {
for (const patch of contextMenuPatches) { for (const patch of contextMenuPatches) {
try { try {
patch(props.children, props.contextMenuApiArguments); patch(props.children, ...props.contextMenuApiArguments);
} catch (err) { } catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
} }
@ -133,7 +134,7 @@ export function _patchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) { for (const patch of globalPatches) {
try { try {
patch(props.navId, props.children, props.contextMenuApiArguments); patch(props.navId, props.children, ...props.contextMenuApiArguments);
} catch (err) { } catch (err) {
ContextMenuLogger.error("Global patch errored,", err); ContextMenuLogger.error("Global patch errored,", err);
} }

View File

@ -63,7 +63,10 @@ export default ErrorBoundary.wrap(function NotificationComponent({
<button <button
className="vc-notification-root" className="vc-notification-root"
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }} style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
onClick={onClick} onClick={() => {
onClose!();
onClick?.();
}}
onContextMenu={e => { onContextMenu={e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -78,7 +81,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
<div className="vc-notification-header"> <div className="vc-notification-header">
<h2 className="vc-notification-title">{title}</h2> <h2 className="vc-notification-title">{title}</h2>
<button <button
style={{ all: "unset", cursor: "pointer" }} className="vc-notification-close-btn"
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -86,7 +89,6 @@ export default ErrorBoundary.wrap(function NotificationComponent({
}} }}
> >
<svg <svg
className="vc-notification-close-btn"
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"

View File

@ -40,6 +40,8 @@
} }
.vc-notification-close-btn { .vc-notification-close-btn {
all: unset;
cursor: pointer;
color: var(--interactive-normal); color: var(--interactive-normal);
opacity: 0.5; opacity: 0.5;
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out; transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;

View File

@ -28,6 +28,7 @@ const logger = new Logger("Settings");
export interface Settings { export interface Settings {
notifyAboutUpdates: boolean; notifyAboutUpdates: boolean;
autoUpdate: boolean; autoUpdate: boolean;
autoUpdateNotification: boolean,
useQuickCss: boolean; useQuickCss: boolean;
enableReactDevtools: boolean; enableReactDevtools: boolean;
themeLinks: string[]; themeLinks: string[];
@ -52,6 +53,7 @@ export interface Settings {
const DefaultSettings: Settings = { const DefaultSettings: Settings = {
notifyAboutUpdates: true, notifyAboutUpdates: true,
autoUpdate: false, autoUpdate: false,
autoUpdateNotification: true,
useQuickCss: true, useQuickCss: true,
themeLinks: [], themeLinks: [],
enableReactDevtools: false, enableReactDevtools: false,

View File

@ -19,7 +19,8 @@
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { makeCodeblock } from "@utils/misc"; import { makeCodeblock } from "@utils/misc";
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { ReplaceFn } from "@utils/types";
import { search } from "@webpack"; import { search } from "@webpack";
import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common"; import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common";

View File

@ -38,9 +38,12 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
function handleChange(newValue) { function handleChange(newValue) {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true; const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
setError(null);
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
setState(`${Number.MAX_SAFE_INTEGER}`); setState(`${Number.MAX_SAFE_INTEGER}`);
onChange(serialize(newValue)); onChange(serialize(newValue));
} else { } else {

View File

@ -36,6 +36,7 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else { else {
setError(null);
setState(newValue); setState(newValue);
onChange(newValue); onChange(newValue);
} }

View File

@ -34,6 +34,7 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else { else {
setError(null);
setState(newValue); setState(newValue);
onChange(newValue); onChange(newValue);
} }

View File

@ -18,6 +18,7 @@
import "./Switch.css"; import "./Switch.css";
import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
interface SwitchProps { interface SwitchProps {
@ -33,7 +34,7 @@ const SwitchClasses = findByPropsLazy("slider", "input", "container");
export function Switch({ checked, onChange, disabled }: SwitchProps) { export function Switch({ checked, onChange, disabled }: SwitchProps) {
return ( return (
<div> <div>
<div className={`${SwitchClasses.container} default-colors`} style={{ <div className={classes(SwitchClasses.container, "default-colors", checked ? SwitchClasses.checked : void 0)} style={{
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF, backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
opacity: disabled ? 0.3 : 1 opacity: disabled ? 0.3 : 1
}}> }}>

View File

@ -185,7 +185,7 @@ function Newer(props: CommonProps) {
} }
function Updater() { function Updater() {
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]); const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." }); const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
@ -205,7 +205,7 @@ function Updater() {
<Switch <Switch
value={settings.notifyAboutUpdates} value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v} onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a toast on startup" note="Shows a notification on startup"
disabled={settings.autoUpdate} disabled={settings.autoUpdate}
> >
Get notified about new updates Get notified about new updates
@ -217,6 +217,14 @@ function Updater() {
> >
Automatically update Automatically update
</Switch> </Switch>
<Switch
value={settings.autoUpdateNotification}
onChange={(v: boolean) => settings.autoUpdateNotification = v}
note="Shows a notification when Vencord automatically updates"
disabled={!settings.autoUpdate}
>
Get notified when an automatic update completes
</Switch>
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle> <Forms.FormTitle tag="h5">Repo</Forms.FormTitle>

View File

@ -18,9 +18,28 @@
import { Settings } from "@api/settings"; import { Settings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin, { type PatchReplacement } from "@utils/types";
import { addListener, removeListener } from "@webpack"; import { addListener, removeListener } from "@webpack";
/**
* The last var name corresponding to the Context Menu API (Discord, not ours) module
*/
let lastVarName = "";
/**
* @param target The patch replacement object
* @param exportKey The key exporting the build Context Menu component function
*/
function makeReplacementProxy(target: PatchReplacement, exportKey: string) {
return new Proxy(target, {
get(_, p) {
if (p === "match") return RegExp(`${exportKey},{(?<=${lastVarName}\\.${exportKey},{)`, "g");
// @ts-expect-error
return Reflect.get(...arguments);
}
});
}
function listener(exports: any, id: number) { function listener(exports: any, id: number) {
if (!Settings.plugins.ContextMenuAPI.enabled) return removeListener(listener); if (!Settings.plugins.ContextMenuAPI.enabled) return removeListener(listener);
@ -37,13 +56,24 @@ function listener(exports: any, id: number) {
all: true, all: true,
noWarn: true, noWarn: true,
find: "navId:", find: "navId:",
replacement: [{ replacement: [
match: RegExp(`${id}(?<=(\\i)=.+?).+$`), {
replace: (code, varName) => { // Set the lastVarName for our proxy to use
const regex = RegExp(`${key},{(?<=${varName}\\.${key},{)`, "g"); match: RegExp(`${id}(?<=(\\i)=.+?)`),
return code.replace(regex, "$&contextMenuApiArguments:arguments,"); replace: (id, varName) => {
} lastVarName = varName;
}] return id;
}
},
/**
* We are using a proxy here to utilize the whole code the patcher gives us, instead of matching the entire module (which is super slow)
* Our proxy returns the corresponding match for that module utilizing lastVarName, which is set by the patch before
*/
makeReplacementProxy({
match: "", // Needed to canonicalizeDescriptor
replace: "$&contextMenuApiArguments:arguments,",
}, key)
]
}); });
removeListener(listener); removeListener(listener);

View File

@ -27,7 +27,7 @@ export default definePlugin({
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: { replacement: {
// foo && !bar ? createElement(blah,...makeElement(addReactionData)) // foo && !bar ? createElement(blah,...makeElement(addReactionData))
match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/, match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,200}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
replace: (m, bools, makeElement) => { replace: (m, bools, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1]; const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable"); if (!msg) throw new Error("Could not find message variable");

View File

@ -27,11 +27,16 @@ export default definePlugin({
{ {
find: "Masks.STATUS_ONLINE", find: "Masks.STATUS_ONLINE",
replacement: { replacement: {
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
// so it keeps the patch and plugin small and simple
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g, match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
replace: "Masks.STATUS_ONLINE" replace: "Masks.STATUS_ONLINE"
} }
},
{
find: ".AVATAR_STATUS_MOBILE_16;",
replacement: {
match: /(\.fromIsMobile,.+?)\i.status/,
replace: (_, rest) => `${rest}"online"`
}
} }
] ]
}); });

View File

@ -42,6 +42,7 @@ const settings = definePluginSettings({
}); });
let crashCount: number = 0; let crashCount: number = 0;
let lastCrashTimestamp: number = 0;
export default definePlugin({ export default definePlugin({
name: "CrashHandler", name: "CrashHandler",
@ -80,6 +81,7 @@ export default definePlugin({
}); });
} catch { } } catch { }
lastCrashTimestamp = Date.now();
return false; return false;
} }
@ -97,17 +99,21 @@ export default definePlugin({
} catch (err) { } catch (err) {
CrashHandlerLogger.error("Failed to handle crash", err); CrashHandlerLogger.error("Failed to handle crash", err);
return false; return false;
} finally {
lastCrashTimestamp = Date.now();
} }
}, },
handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) { handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) {
try { if (Date.now() - lastCrashTimestamp >= 1_000) {
showNotification({ try {
color: "#eed202", showNotification({
title: "Discord has crashed!", color: "#eed202",
body: "Attempting to recover...", title: "Discord has crashed!",
}); body: "Attempting to recover...",
} catch { } });
} catch { }
}
try { try {
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" }); FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });

View File

@ -16,12 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addContextMenuPatch } from "@api/ContextMenu"; import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { showNotification } from "@api/Notifications"; import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { filters, findAll, search } from "@webpack"; import { filters, findAll, search } from "@webpack";
import { Menu } from "@webpack/common"; import { Menu } from "@webpack/common";
@ -65,6 +66,14 @@ interface FindData {
args: Array<StringNode | FunctionNode>; args: Array<StringNode | FunctionNode>;
} }
const settings = definePluginSettings({
notifyOnAutoConnect: {
description: "Whether to notify when Dev Companion has automatically connected.",
type: OptionType.BOOLEAN,
default: true
}
});
function parseNode(node: Node) { function parseNode(node: Node) {
switch (node.type) { switch (node.type) {
case "string": case "string":
@ -91,7 +100,7 @@ function initWs(isManual = false) {
logger.info("Connected to WebSocket"); logger.info("Connected to WebSocket");
showNotification({ (settings.store.notifyOnAutoConnect || isManual) && showNotification({
title: "Dev Companion Connected", title: "Dev Companion Connected",
body: "Connected to WebSocket" body: "Connected to WebSocket"
}); });
@ -221,32 +230,36 @@ function initWs(isManual = false) {
}); });
} }
const contextMenuPatch: NavContextMenuPatchCallback = kids => {
if (kids.some(k => k?.props?.id === NAV_ID)) return;
kids.unshift(
<Menu.MenuItem
id={NAV_ID}
label="Reconnect Dev Companion"
action={() => {
socket?.close(1000, "Reconnecting");
initWs(true);
}}
/>
);
};
export default definePlugin({ export default definePlugin({
name: "DevCompanion", name: "DevCompanion",
description: "Dev Companion Plugin", description: "Dev Companion Plugin",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["ContextMenuAPI"], dependencies: ["ContextMenuAPI"],
settings,
start() { start() {
initWs(); initWs();
addContextMenuPatch("user-settings-cog", kids => { addContextMenuPatch("user-settings-cog", contextMenuPatch);
if (kids.some(k => k?.props?.id === NAV_ID)) return;
kids.unshift(
<Menu.MenuItem
id={NAV_ID}
label="Reconnect Dev Companion"
action={() => {
socket?.close(1000, "Reconnecting");
initWs(true);
}}
/>
);
});
}, },
stop() { stop() {
socket?.close(1000, "Plugin Stopped"); socket?.close(1000, "Plugin Stopped");
socket = void 0; socket = void 0;
removeContextMenuPatch("user-settings-cog", contextMenuPatch);
} }
}); });

View File

@ -176,9 +176,9 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
); );
} }
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, args) => { const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (!args?.[0]) return; if (!props) return;
const { favoriteableId, emoteClonerDataAlt, itemHref, itemSrc, favoriteableType } = args[0]; const { favoriteableId, emoteClonerDataAlt, itemHref, itemSrc, favoriteableType } = props;
if (!emoteClonerDataAlt || favoriteableType !== "emoji") return; if (!emoteClonerDataAlt || favoriteableType !== "emoji") return;
@ -188,7 +188,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, args) =>
const src = itemHref ?? itemSrc; const src = itemHref ?? itemSrc;
const isAnimated = new URL(src).pathname.endsWith(".gif"); const isAnimated = new URL(src).pathname.endsWith(".gif");
const group = findGroupChildrenByChildId("save-image", children); const group = findGroupChildrenByChildId("copy-link", children);
if (group && !group.some(child => child?.props?.id === "emote-cloner")) { if (group && !group.some(child => child?.props?.id === "emote-cloner")) {
group.push(( group.push((
<Menu.MenuItem <Menu.MenuItem

42
src/plugins/f8break.ts Normal file
View File

@ -0,0 +1,42 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "F8Break",
description: "Pause the client when you press F8 with DevTools (+ breakpoints) open.",
authors: [Devs.lewisakura],
start() {
window.addEventListener("keydown", this.event);
},
stop() {
window.removeEventListener("keydown", this.event);
},
event(e: KeyboardEvent) {
if (e.code === "F8") {
// Hi! You've just paused the client. Pressing F8 in DevTools or in the main window will unpause it again.
// It's up to you on what to do, friend. Happy travels!
debugger;
}
}
});

View File

@ -20,12 +20,15 @@ import { addPreEditListener, addPreSendListener, removePreEditListener, removePr
import { migratePluginSettings, Settings } from "@api/settings"; import { migratePluginSettings, Settings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies"; import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { ChannelStore, PermissionStore, UserStore } from "@webpack/common"; import { ChannelStore, FluxDispatcher, PermissionStore, UserStore } from "@webpack/common";
const DRAFT_TYPE = 0; const DRAFT_TYPE = 0;
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
const ProtoPreloadedUserSettings = findLazy(m => m.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings");
const USE_EXTERNAL_EMOJIS = 1n << 18n; const USE_EXTERNAL_EMOJIS = 1n << 18n;
const USE_EXTERNAL_STICKERS = 1n << 37n; const USE_EXTERNAL_STICKERS = 1n << 37n;
@ -86,12 +89,12 @@ export default definePlugin({
replace: (_, intention) => `,fakeNitroIntention=${intention}` replace: (_, intention) => `,fakeNitroIntention=${intention}`
}, },
{ {
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g, match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
replace: ',typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0' replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
}, },
{ {
match: /(?<=&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
replace: (_, canUseExternal) => `(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))` replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
} }
] ]
}, },
@ -99,16 +102,16 @@ export default definePlugin({
find: "canUseAnimatedEmojis:function", find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true, predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
replacement: { replacement: {
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g, match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g,
replace: (_, premiumCheck) => `,fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
} }
}, },
{ {
find: "canUseStickersEverywhere:function", find: "canUseStickersEverywhere:function",
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true, predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
replacement: { replacement: {
match: /(?<=canUseStickersEverywhere:function\(\i\){)/, match: /canUseStickersEverywhere:function\(\i\){/,
replace: "return true;" replace: "$&return true;"
}, },
}, },
{ {
@ -128,8 +131,8 @@ export default definePlugin({
"canStreamMidQuality" "canStreamMidQuality"
].map(func => { ].map(func => {
return { return {
match: new RegExp(`(?<=${func}:function\\(\\i\\){)`), match: new RegExp(`${func}:function\\(\\i\\){`),
replace: "return true;" replace: "$&return true;"
}; };
}) })
}, },
@ -144,8 +147,28 @@ export default definePlugin({
{ {
find: "canUseClientThemes:function", find: "canUseClientThemes:function",
replacement: { replacement: {
match: /(?<=canUseClientThemes:function\(\i\){)/, match: /canUseClientThemes:function\(\i\){/,
replace: "return true;" replace: "$&return true;"
}
},
{
find: '.displayName="UserSettingsProtoStore"',
replacement: [
{
match: /CONNECTION_OPEN:function\((\i)\){/,
replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
},
{
match: /=(\i)\.local;/,
replace: (m, props) => `${m}${props}.local||$self.handleProtoChange(${props}.settings.proto);`
}
]
},
{
find: "updateTheme:function",
replacement: {
match: /(function \i\(\i\){var (\i)=\i\.backgroundGradientPresetId.+?)\i\.\i\.updateAsync.+?theme=(.+?);.+?\),\i\)/,
replace: (_, rest, backgroundGradientPresetId, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme});`
} }
} }
], ],
@ -184,7 +207,7 @@ export default definePlugin({
}, },
get guildId() { get guildId() {
return window.location.href.split("channels/")[1].split("/")[0]; return getCurrentGuild()?.id;
}, },
get canUseEmotes() { get canUseEmotes() {
@ -195,6 +218,60 @@ export default definePlugin({
return (UserStore.getCurrentUser().premiumType ?? 0) > 1; return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
}, },
handleProtoChange(proto: any, user: any) {
const premiumType: number = user?.premium_type ?? UserStore.getCurrentUser()?.premiumType ?? 0;
if (premiumType === 0) {
const appearanceDummyProto = ProtoPreloadedUserSettings.create({
appearance: {}
});
proto.appearance ??= appearanceDummyProto.appearance;
if (UserSettingsProtoStore.settings.appearance?.theme != null) {
proto.appearance.theme = UserSettingsProtoStore.settings.appearance.theme;
}
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null) {
const clientThemeSettingsDummyProto = ProtoPreloadedUserSettings.create({
appearance: {
clientThemeSettings: {
backgroundGradientPresetId: {
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
}
}
}
});
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummyProto.appearance.clientThemeSettings;
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.appearance.clientThemeSettings.backgroundGradientPresetId;
}
}
},
handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number) {
const proto = ProtoPreloadedUserSettings.create({
appearance: {
theme,
clientThemeSettings: {
backgroundGradientPresetId: backgroundGradientPresetId != null ? {
value: backgroundGradientPresetId
} : void 0
}
}
});
FluxDispatcher.dispatch({
type: "USER_SETTINGS_PROTO_UPDATE",
local: true,
partial: true,
settings: {
type: 1,
proto
}
});
},
hasPermissionToUseExternalEmojis(channelId: string) { hasPermissionToUseExternalEmojis(channelId: string) {
const channel = ChannelStore.getChannel(channelId); const channel = ChannelStore.getChannel(channelId);

View File

@ -23,7 +23,7 @@ import { findByProps } from "@webpack";
export default definePlugin({ export default definePlugin({
name: "FriendInvites", name: "FriendInvites",
description: "Generate and manage friend invite links.", description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).",
authors: [Devs.afn], authors: [Devs.afn],
dependencies: ["CommandsAPI"], dependencies: ["CommandsAPI"],
commands: [ commands: [
@ -37,8 +37,8 @@ export default definePlugin({
return void sendBotMessage(ctx.channel.id, { return void sendBotMessage(ctx.channel.id, {
content: ` content: `
discord.gg/${createInvite.code} discord.gg/${createInvite.code} ·
Expires: <t:${new Date(createInvite.expires_at).getTime() / 1000}:R> Expires: <t:${new Date(createInvite.expires_at).getTime() / 1000}:R> ·
Max uses: \`${createInvite.max_uses}\` Max uses: \`${createInvite.max_uses}\`
`.trim().replace(/\s+/g, " ") `.trim().replace(/\s+/g, " ")
}); });
@ -52,25 +52,25 @@ export default definePlugin({
const friendInvites = findByProps("createFriendInvite"); const friendInvites = findByProps("createFriendInvite");
const invites = await friendInvites.getAllFriendInvites(); const invites = await friendInvites.getAllFriendInvites();
const friendInviteList = invites.map(i => const friendInviteList = invites.map(i =>
`_discord.gg/${i.code}_ `_discord.gg/${i.code}_ ·
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> ·
Times used: \`${i.uses}/${i.max_uses}\``.trim().replace(/\s+/g, " ") Times used: \`${i.uses}/${i.max_uses}\``.trim().replace(/\s+/g, " ")
); );
return void sendBotMessage(ctx.channel.id, { return void sendBotMessage(ctx.channel.id, {
content: friendInviteList.join("\n\n") || "You have no active friend invites!" content: friendInviteList.join("\n") || "You have no active friend invites!"
}); });
}, },
}, },
{ {
name: "revoke friend invites", name: "revoke friend invites",
description: "Revokes ALL generated friend invite links.", description: "Revokes all generated friend invites.",
inputType: ApplicationCommandInputType.BOT, inputType: ApplicationCommandInputType.BOT,
execute: async (_, ctx) => { execute: async (_, ctx) => {
await findByProps("createFriendInvite").revokeFriendInvites(); await findByProps("createFriendInvite").revokeFriendInvites();
return void sendBotMessage(ctx.channel.id, { return void sendBotMessage(ctx.channel.id, {
content: "All friend links have been revoked." content: "All friend invites have been revoked."
}); });
}, },
}, },

View File

@ -131,8 +131,8 @@ export default definePlugin({
{ {
find: ".activeCommandOption", find: ".activeCommandOption",
replacement: { replacement: {
match: /(.)\.push.{1,50}\(\i,\{.{1,30}\},"gift"\)\)/, match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&;try{$1.push($self.chatBarIcon())}catch{}", replace: "$&;try{$2||$1.push($self.chatBarIcon())}catch{}",
} }
}, },
], ],

View File

@ -20,18 +20,20 @@ import { addClickListener, removeClickListener } from "@api/MessageEvents";
import { migratePluginSettings } from "@api/settings"; import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { UserStore } from "@webpack/common"; import { PermissionStore, UserStore } from "@webpack/common";
let isDeletePressed = false; let isDeletePressed = false;
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true); const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false); const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
const MANAGE_CHANNELS = 1n << 4n;
migratePluginSettings("MessageClickActions", "MessageQuickActions"); migratePluginSettings("MessageClickActions", "MessageQuickActions");
export default definePlugin({ export default definePlugin({
name: "MessageClickActions", name: "MessageClickActions",
description: "Hold Delete and click to delete, double click to edit", description: "Hold Backspace and click to delete, double click to edit",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["MessageEventsAPI"], dependencies: ["MessageEventsAPI"],
@ -50,8 +52,6 @@ export default definePlugin({
start() { start() {
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
const PermissionStore = findByPropsLazy("can", "initialize");
const Permissions = findLazy(m => typeof m.MANAGE_MESSAGES === "bigint");
const EditStore = findByPropsLazy("isEditing", "isEditingAny"); const EditStore = findByPropsLazy("isEditing", "isEditingAny");
document.addEventListener("keydown", keydown); document.addEventListener("keydown", keydown);
@ -64,7 +64,7 @@ export default definePlugin({
MessageActions.startEditMessage(chan.id, msg.id, msg.content); MessageActions.startEditMessage(chan.id, msg.id, msg.content);
event.preventDefault(); event.preventDefault();
} }
} else if (Vencord.Settings.plugins.MessageClickActions.enableDeleteOnClick && (isMe || PermissionStore.can(Permissions.MANAGE_MESSAGES, chan))) { } else if (Vencord.Settings.plugins.MessageClickActions.enableDeleteOnClick && (isMe || PermissionStore.can(MANAGE_CHANNELS, chan))) {
MessageActions.deleteMessage(chan.id, msg.id); MessageActions.deleteMessage(chan.id, msg.id);
event.preventDefault(); event.preventDefault();
} }

View File

@ -24,11 +24,15 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy } from "@webpack";
import { moment, Parser, Timestamp, UserStore } from "@webpack/common"; import { moment, Parser, Timestamp, UserStore } from "@webpack/common";
import overlayStyle from "./deleteStyleOverlay.css?managed"; import overlayStyle from "./deleteStyleOverlay.css?managed";
import textStyle from "./deleteStyleText.css?managed"; import textStyle from "./deleteStyleText.css?managed";
const i18n = findLazy(m => m.Messages?.["en-US"]);
const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessage");
function addDeleteStyle() { function addDeleteStyle() {
if (Settings.plugins.MessageLogger.deleteStyle === "text") { if (Settings.plugins.MessageLogger.deleteStyle === "text") {
enableStyle(textStyle); enableStyle(textStyle);
@ -65,7 +69,7 @@ export default definePlugin({
isEdited={true} isEdited={true}
isInline={false} isInline={false}
> >
<span>{" "}(edited)</span> <span className={styles.edited}>{" "}({i18n.Messages.MESSAGE_EDITED})</span>
</Timestamp> </Timestamp>
</div> </div>
</ErrorBoundary> </ErrorBoundary>
@ -170,6 +174,7 @@ export default definePlugin({
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/, match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
replace: "$1" + replace: "$1" +
".update($3,m =>" + ".update($3,m =>" +
" (($2.message.flags & 64) === 64 || (Vencord.Settings.plugins.MessageLogger.ignoreBots && $2.message.author?.bot) || (Vencord.Settings.plugins.MessageLogger.ignoreSelf && $2.message.author?.id === Vencord.Webpack.Common.UserStore.getCurrentUser().id)) ? m :" +
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" + " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" + " m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" + " m" +

View File

@ -37,16 +37,19 @@ export default definePlugin({
} }
] ]
}, },
{ ...[
find: "displayName=\"MessageStore\"", 'displayName="MessageStore"',
'displayName="ReadStateStore"'
].map(find => ({
find,
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true, predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,
replacement: [ replacement: [
{ {
match: /(?<=MESSAGE_CREATE:function\((\w)\){var \w=\w\.channelId,\w=\w\.message,\w=\w\.isPushNotification,\w=\w\.\w\.getOrCreate\(\w\));/, match: /(?<=MESSAGE_CREATE:function\((\i)\){)/,
replace: ";if($self.isBlocked(n))return;" replace: (_, props) => `if($self.isBlocked(${props}.message))return;`
} }
] ]
} }))
], ],
options: { options: {
ignoreBlockedMessages: { ignoreBlockedMessages: {

View File

@ -23,11 +23,11 @@ import { Settings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByCodeLazy, findStoreLazy } from "@webpack";
import { PresenceStore, Tooltip, UserStore } from "@webpack/common"; import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
const SessionStore = findByPropsLazy("getActiveSession"); const SessionsStore = findStoreLazy("SessionsStore");
function Icon(path: string, viewBox = "0 0 24 24") { function Icon(path: string, viewBox = "0 0 24 24") {
return ({ color, tooltip }: { color: string; tooltip: string; }) => ( return ({ color, tooltip }: { color: string; tooltip: string; }) => (
@ -70,7 +70,7 @@ const PlatformIndicator = ({ user, inline = false, marginLeft = "4px" }: { user:
if (!user || user.bot) return null; if (!user || user.bot) return null;
if (user.id === UserStore.getCurrentUser().id) { if (user.id === UserStore.getCurrentUser().id) {
const sessions = SessionStore.getSessions(); const sessions = SessionsStore.getSessions();
if (typeof sessions !== "object") return null; if (typeof sessions !== "object") return null;
const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => { const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => {
if (a === b) return 0; if (a === b) return 0;
@ -156,7 +156,7 @@ const indicatorLocations = {
export default definePlugin({ export default definePlugin({
name: "PlatformIndicators", name: "PlatformIndicators",
description: "Adds platform indicators (Desktop, Mobile, Web...) to users", description: "Adds platform indicators (Desktop, Mobile, Web...) to users",
authors: [Devs.kemo, Devs.TheSun], authors: [Devs.kemo, Devs.TheSun, Devs.Nuckyz],
dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"], dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"],
start() { start() {
@ -185,6 +185,55 @@ export default definePlugin({
}); });
}, },
patches: [
{
find: ".Masks.STATUS_ONLINE_MOBILE",
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
replacement: [
{
// Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status
match: /(?<=return \i\.\i\.Masks\.STATUS_TYPING;)(.+?)(\i)\?(\i\.\i\.Masks\.STATUS_ONLINE_MOBILE):/,
replace: (_, rest, isMobile, mobileMask) => `if(${isMobile})return ${mobileMask};${rest}`
},
{
// Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status
match: /(switch\(\i\){case \i\.\i\.ONLINE:return )(\i)\?({.+?}):/,
replace: (_, rest, isMobile, component) => `if(${isMobile})return${component};${rest}`
}
]
},
{
find: ".AVATAR_STATUS_MOBILE_16;",
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
replacement: [
{
// Return the AVATAR_STATUS_MOBILE size mask if the user is on mobile, no matter the status
match: /\i===\i\.\i\.ONLINE&&(?=.{0,70}\.AVATAR_STATUS_MOBILE_16;)/,
replace: ""
},
{
// Fix sizes for mobile indicators which aren't online
match: /(?<=\(\i\.status,)(\i)(?=,(\i),\i\))/,
replace: (_, userStatus, isMobile) => `${isMobile}?"online":${userStatus}`
},
{
// Make isMobile true no matter the status
match: /(?<=\i&&!\i)&&\i===\i\.\i\.ONLINE/,
replace: ""
}
]
},
{
find: "isMobileOnline=function",
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
replacement: {
// Make isMobileOnline return true no matter what is the user status
match: /(?<=\i\[\i\.\i\.MOBILE\])===\i\.\i\.ONLINE/,
replace: "!= null"
}
}
],
options: { options: {
...Object.fromEntries( ...Object.fromEntries(
Object.entries(indicatorLocations).map(([key, value]) => { Object.entries(indicatorLocations).map(([key, value]) => {
@ -196,6 +245,12 @@ export default definePlugin({
default: true default: true
}]; }];
}) })
) ),
colorMobileIndicator: {
type: OptionType.BOOLEAN,
description: "Whether to make the mobile indicator match the color of the user status.",
default: true,
restartNeeded: true
}
} }
}); });

View File

@ -34,15 +34,15 @@ function search(src: string, engine: string) {
open(engine + encodeURIComponent(src), "_blank"); open(engine + encodeURIComponent(src), "_blank");
} }
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, args) => { const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (!args?.[0]) return; if (!props) return;
const { reverseImageSearchType, itemHref, itemSrc } = args[0]; const { reverseImageSearchType, itemHref, itemSrc } = props;
if (!reverseImageSearchType || reverseImageSearchType !== "img") return; if (!reverseImageSearchType || reverseImageSearchType !== "img") return;
const src = itemHref ?? itemSrc; const src = itemHref ?? itemSrc;
const group = findGroupChildrenByChildId("save-image", children); const group = findGroupChildrenByChildId("copy-link", children);
if (group && !group.some(child => child?.props?.id === "search-image")) { if (group && !group.some(child => child?.props?.id === "search-image")) {
group.push(( group.push((
<Menu.MenuItem <Menu.MenuItem

View File

@ -52,8 +52,8 @@ export default definePlugin({
find: 'className:"mention"', find: 'className:"mention"',
replacement: [ replacement: [
{ {
match: /user:(\i),channelId:(\i).{0,300}?"@"\.concat\(.+?\)/, match: /user:(\i),channel:(\i).{0,300}?"@"\.concat\(.+?\)/,
replace: "$&,color:$self.getUserColor($1.id,{channelId:$2})" replace: "$&,color:$self.getUserColor($1.id,{channelId:$2?.id})"
} }
], ],
predicate: () => settings.store.chatMentions, predicate: () => settings.store.chatMentions,
@ -65,7 +65,7 @@ export default definePlugin({
replacement: [ replacement: [
{ {
match: /function \i\((\i)\).{5,20}id.{5,20}guildId.{5,10}channelId.{100,150}hidePersonalInformation.{5,50}jsx.{5,20},{/, match: /function \i\((\i)\).{5,20}id.{5,20}guildId.{5,10}channelId.{100,150}hidePersonalInformation.{5,50}jsx.{5,20},{/,
replace: "$&color:$self.getUserColor($1.id,{guildId:$1.guildId})," replace: "$&color:$self.getUserColor($1.id,{guildId:$1?.guildId}),"
} }
], ],
predicate: () => settings.store.chatMentions, predicate: () => settings.store.chatMentions,

View File

@ -20,10 +20,12 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { LazyComponent } from "@utils/misc"; import { LazyComponent } from "@utils/misc";
import { formatDuration } from "@utils/text"; import { formatDuration } from "@utils/text";
import { find, findByPropsLazy } from "@webpack"; import { find, findByPropsLazy } from "@webpack";
import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common"; import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general"; import type { Channel } from "discord-types/general";
import type { ComponentType } from "react"; import type { ComponentType } from "react";
import { VIEW_CHANNEL } from "..";
enum SortOrderTypes { enum SortOrderTypes {
LATEST_ACTIVITY = 0, LATEST_ACTIVITY = 0,
@ -167,7 +169,7 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
<img className="shc-lock-screen-logo" src={HiddenChannelLogo} /> <img className="shc-lock-screen-logo" src={HiddenChannelLogo} />
<div className="shc-lock-screen-heading-container"> <div className="shc-lock-screen-heading-container">
<Text variant="heading-xxl/bold">This is a hidden {ChannelTypesToChannelNames[type]} channel.</Text> <Text variant="heading-xxl/bold">This is a {!PermissionStore.can(VIEW_CHANNEL, channel) ? "hidden" : "locked"} {ChannelTypesToChannelNames[type]} channel.</Text>
{channel.isNSFW() && {channel.isNSFW() &&
<Tooltip text="NSFW"> <Tooltip text="NSFW">
{({ onMouseLeave, onMouseEnter }) => ( {({ onMouseLeave, onMouseEnter }) => (

View File

@ -21,6 +21,7 @@ import "./style.css";
import { definePluginSettings } from "@api/settings"; import { definePluginSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common"; import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common";
@ -30,7 +31,8 @@ import HiddenChannelLockScreen, { setChannelBeginHeaderComponent, setEmojiCompon
const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer"); const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer");
const VIEW_CHANNEL = 1n << 10n; export const VIEW_CHANNEL = 1n << 10n;
const CONNECT = 1n << 20n;
enum ShowMode { enum ShowMode {
LockIcon, LockIcon,
@ -99,14 +101,14 @@ export default definePlugin({
replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&` replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
}, },
{ {
// Make Discord think we are connected to a voice channel so it shows us inside it // Prevent Discord from trying to connect to hidden channels
match: /(?=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\))/, match: /(?=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\))/,
replace: (_, channel) => `||$self.isHiddenChannel(${channel})` replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
}, },
{ {
// Make Discord think we are connected to a voice channel so it shows us inside it // Make Discord show inside the channel if clicking on a hidden or locked channel
match: /(?<=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\);!__OVERLAY__&&\()/, match: /(?<=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\);!__OVERLAY__&&\()/,
replace: (_, channel) => `$self.isHiddenChannel(${channel})||` replace: (_, channel) => `$self.isHiddenChannel(${channel},true)||`
} }
] ]
}, },
@ -244,14 +246,54 @@ export default definePlugin({
find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE", find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE",
replacement: [ replacement: [
{ {
// Export the channel beggining header // Export the channel beginning header
match: /computePermissionsForRoles.+?}\)}(?<=function (\i)\(.+?)(?=var)/, match: /computePermissionsForRoles.+?}\)}(?<=function (\i)\(.+?)(?=var)/,
replace: (m, component) => `${m}$self.setChannelBeginHeaderComponent(${component});` replace: (m, component) => `${m}$self.setChannelBeginHeaderComponent(${component});`
}, },
{ {
// Patch the header to only return allowed users and roles if it's a hidden channel (Like when it's used on the HiddenChannelLockScreen) // Change the role permission check to CONNECT if the channel is locked
match: /ADMINISTRATOR\)\|\|(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
},
{
// Change the permissionOverwrite check to CONNECT if the channel is locked
match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
},
{
// Patch the header to only return allowed users and roles if it's a hidden channel or locked channel (Like when it's used on the HiddenChannelLockScreen)
match: /MANAGE_ROLES.{0,60}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?]}\)))/, match: /MANAGE_ROLES.{0,60}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?]}\)))/,
replace: (m, component, channel) => `${m} $self.isHiddenChannel(${channel})?${component}:` replace: (m, component, channel) => {
// Export the channel for the users allowed component patch
component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,channel:${channel}`);
return `${m} $self.isHiddenChannel(${channel},true)?${component}:`;
}
}
]
},
{
find: "().avatars),children",
replacement: [
{
// Create a variable for the channel prop
match: /=(\i)\.maxUsers,/,
replace: (m, props) => `${m}channel=${props}.channel,`
},
{
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
match: /\i>0(?=&&.{0,60}renderPopout)/,
replace: m => `($self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)?true:${m})`
},
{
// Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
replace: (_, amount) => `($self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)&&${amount}<=0?0:1)`
},
{
// Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
match: /(?<="\+",)(\i)\+1/,
replace: (m, amount) => `$self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)&&${amount}<=0?"":${m}`
} }
] ]
}, },
@ -261,22 +303,22 @@ export default definePlugin({
{ {
// Remove the divider and the open chat button for the HiddenChannelLockScreen // Remove the divider and the open chat button for the HiddenChannelLockScreen
match: /"more-options-popout"\)\);if\((?<=function \i\((\i)\).+?)/, match: /"more-options-popout"\)\);if\((?<=function \i\((\i)\).+?)/,
replace: (m, props) => `${m}(!$self.isHiddenChannel(${props}.channel)||${props}.inCall)&&` replace: (m, props) => `${m}!${props}.inCall&&$self.isHiddenChannel(${props}.channel,true)){}else if(`
}, },
{ {
// Render our HiddenChannelLockScreen component instead of the main voice channel component // Render our HiddenChannelLockScreen component instead of the main voice channel component
match: /this\.renderVoiceChannelEffects.+?children:(?<=renderContent=function.+?)/, match: /this\.renderVoiceChannelEffects.+?children:(?<=renderContent=function.+?)/,
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?$self.HiddenChannelLockScreen(this.props.channel):" replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?$self.HiddenChannelLockScreen(this.props.channel):"
}, },
{ {
// Disable gradients for the HiddenChannelLockScreen of voice channels // Disable gradients for the HiddenChannelLockScreen of voice channels
match: /this\.renderVoiceChannelEffects.+?disableGradients:(?<=renderContent=function.+?)/, match: /this\.renderVoiceChannelEffects.+?disableGradients:(?<=renderContent=function.+?)/,
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)||" replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)||"
}, },
{ {
// Disable useless components for the HiddenChannelLockScreen of voice channels // Disable useless components for the HiddenChannelLockScreen of voice channels
match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:(?<=renderContent=function.+?)(?!void)/g, match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:(?<=renderContent=function.+?)(?!void)/g,
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?null:" replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?null:"
} }
] ]
}, },
@ -321,9 +363,7 @@ export default definePlugin({
], ],
}, },
{ {
// The module wasn't being found, so lets just escape everything find: "\"^/guild-stages/(\\\\d+)(?:/)?(\\\\d+)?\"",
// eslint-disable-next-line no-useless-escape
find: "\^https\:\/\/\(\?\:canary\.\|ptb\.\)\?discord.com\/channels\/\(\\\\\d\+\|",
replacement: { replacement: {
// Make mentions of hidden channels work // Make mentions of hidden channels work
match: /\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,\i\)/, match: /\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,\i\)/,
@ -337,19 +377,62 @@ export default definePlugin({
match: /(?<=getChannel\((\i)\)\)(?=.{0,100}?selectVoiceChannel))/, match: /(?<=getChannel\((\i)\)\)(?=.{0,100}?selectVoiceChannel))/,
replace: (_, channelId) => `&&!$self.isHiddenChannel({channelId:${channelId}})` replace: (_, channelId) => `&&!$self.isHiddenChannel({channelId:${channelId}})`
} }
},
{
find: '.displayName="GuildChannelStore"',
replacement: [
{
// Make GuildChannelStore contain hidden channels
match: /isChannelGated\(.+?\)(?=\|\|)/,
replace: m => `${m}||true`
},
{
// Filter hidden channels from GuildChannelStore.getChannels unless told otherwise
match: /(?<=getChannels=function\(\i)\).+?(?=return (\i)})/,
replace: (rest, channels) => `,shouldIncludeHidden=false${rest}${channels}=$self.resolveGuildChannels(${channels},shouldIncludeHidden);`
}
]
},
{
find: ".Messages.FORM_LABEL_MUTED",
replacement: {
// Make GuildChannelStore.getChannels return hidden channels
match: /(?<=getChannels\(\i)(?=\))/,
replace: ",true"
}
} }
], ],
setEmojiComponent, setEmojiComponent,
setChannelBeginHeaderComponent, setChannelBeginHeaderComponent,
isHiddenChannel(channel: Channel & { channelId?: string; }) { isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {
if (!channel) return false; if (!channel) return false;
if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId); if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false; if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
return !PermissionStore.can(VIEW_CHANNEL, channel); return !PermissionStore.can(VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(CONNECT, channel);
},
resolveGuildChannels(channels: Record<string | number, Array<{ channel: Channel; comparator: number; }> | string | number>, shouldIncludeHidden: boolean) {
if (shouldIncludeHidden) return channels;
const res = {};
for (const [key, maybeObjChannels] of Object.entries(channels)) {
if (!Array.isArray(maybeObjChannels)) {
res[key] = maybeObjChannels;
continue;
}
res[key] ??= [];
for (const objChannel of maybeObjChannels) {
if (objChannel.channel.id === null || !this.isHiddenChannel(objChannel.channel)) res[key].push(objChannel);
}
}
return res;
}, },
HiddenChannelLockScreen: (channel: any) => <HiddenChannelLockScreen channel={channel} />, HiddenChannelLockScreen: (channel: any) => <HiddenChannelLockScreen channel={channel} />,

View File

@ -103,4 +103,5 @@
.shc-lock-screen-allowed-users-and-roles-container > [class^="members"] { .shc-lock-screen-allowed-users-and-roles-container > [class^="members"] {
margin-left: 10px; margin-left: 10px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
} }

View File

@ -40,28 +40,30 @@ function SilentMessageToggle() {
return ( return (
<Tooltip text="Toggle Silent Message"> <Tooltip text="Toggle Silent Message">
{tooltipProps => ( {tooltipProps => (
<Button <div style={{ display: "flex" }}>
{...tooltipProps} <Button
onClick={() => setEnabled(prev => !prev)} {...tooltipProps}
size="" onClick={() => setEnabled(prev => !prev)}
look={ButtonLooks.BLANK} size=""
innerClassName={ButtonWrapperClasses.button} look={ButtonLooks.BLANK}
style={{ margin: "0px 8px" }} innerClassName={ButtonWrapperClasses.button}
> style={{ margin: "0px 8px" }}
<div className={ButtonWrapperClasses.buttonWrapper}> >
<svg <div className={ButtonWrapperClasses.buttonWrapper}>
width="24" <svg
height="24" width="24"
viewBox="0 0 24 24" height="24"
> viewBox="0 0 24 24"
<g fill="currentColor"> >
<path d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4C13 3.69264 13.0198 3.3899 13.0582 3.093C12.7147 3.03189 12.3611 3 12 3C8.686 3 6 5.686 6 9V14C6 15.657 4.656 17 3 17V18H21V17C19.344 17 18 15.657 18 14V10.7101ZM8.55493 19C9.24793 20.19 10.5239 21 11.9999 21C13.4759 21 14.7519 20.19 15.4449 19H8.55493Z" /> <g fill="currentColor">
<path d="M18.2624 5.50209L21 2.5V1H16.0349V2.49791H18.476L16 5.61088V7H21V5.50209H18.2624Z" /> <path d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4C13 3.69264 13.0198 3.3899 13.0582 3.093C12.7147 3.03189 12.3611 3 12 3C8.686 3 6 5.686 6 9V14C6 15.657 4.656 17 3 17V18H21V17C19.344 17 18 15.657 18 14V10.7101ZM8.55493 19C9.24793 20.19 10.5239 21 11.9999 21C13.4759 21 14.7519 20.19 15.4449 19H8.55493Z" />
{!enabled && <line x1="22" y1="2" x2="2" y2="22" stroke="var(--red-500)" stroke-width="2.5" />} <path d="M18.2624 5.50209L21 2.5V1H16.0349V2.49791H18.476L16 5.61088V7H21V5.50209H18.2624Z" />
</g> {!enabled && <line x1="22" y1="2" x2="2" y2="22" stroke="var(--red-500)" stroke-width="2.5" />}
</svg> </g>
</div> </svg>
</Button> </div>
</Button>
</div>
)} )}
</Tooltip> </Tooltip>
); );
@ -75,8 +77,8 @@ export default definePlugin({
{ {
find: ".activeCommandOption", find: ".activeCommandOption",
replacement: { replacement: {
match: /"gift"\)\);(?<=(\i)\.push.+?)/, match: /"gift"\)\);(?<=(\i)\.push.+?disabled:(\i),.+?)/,
replace: (m, array) => `${m}${array}.push($self.SilentMessageToggle());` replace: (m, array, disabled) => `${m}${disabled}||${array}.push($self.SilentMessageToggle());`
} }
} }
], ],

View File

@ -82,8 +82,8 @@ export default definePlugin({
find: ".activeCommandOption", find: ".activeCommandOption",
predicate: () => settings.store.showIcon, predicate: () => settings.store.showIcon,
replacement: { replacement: {
match: /(.)\.push.{1,50}\(\i,\{.{1,30}\},"gift"\)\)/, match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&;try{$1.push($self.chatBarIcon())}catch{}", replace: "$&;try{$2||$1.push($self.chatBarIcon())}catch{}",
} }
}, },
], ],

View File

@ -22,7 +22,7 @@ import { Devs } from "@utils/constants";
import { LazyComponent } from "@utils/misc"; import { LazyComponent } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { find, findLazy, findStoreLazy } from "@webpack"; import { find, findLazy, findStoreLazy } from "@webpack";
import { ChannelStore, GuildMemberStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; import { ChannelStore, GuildMemberStore, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "./typingTweaks"; import { buildSeveralUsers } from "./typingTweaks";
@ -57,9 +57,9 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
if (isChannelMuted) return null; if (isChannelMuted) return null;
} }
delete typingUsers[UserStore.getCurrentUser().id]; const myId = UserStore.getCurrentUser().id;
const typingUsersArray = Object.keys(typingUsers); const typingUsersArray = Object.keys(typingUsers).filter(id => id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers));
let tooltipText: string; let tooltipText: string;
switch (typingUsersArray.length) { switch (typingUsersArray.length) {
@ -108,13 +108,18 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for muted channels.", description: "Whether to show the typing indicator for muted channels.",
default: false default: false
},
includeBlockedUsers: {
type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for blocked users.",
default: false
} }
}); });
export default definePlugin({ export default definePlugin({
name: "TypingIndicator", name: "TypingIndicator",
description: "Adds an indicator if someone is typing on a channel.", description: "Adds an indicator if someone is typing on a channel.",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz, Devs.obscurity],
settings, settings,
patches: [ patches: [

View File

@ -56,8 +56,8 @@ export default definePlugin({
find: "AudioContextSettingsMigrated", find: "AudioContextSettingsMigrated",
replacement: [ replacement: [
{ {
match: /(?<=updateAsync\("audioContextSettings".{0,50})(?=return (\i)\.volume=(\i))/, match: /(?<=updateAsync\("audioContextSettings".{0,350}return \i\.volume=)\i(?=})/,
replace: (_, volumeOptions, newVolume) => `if(${newVolume}>200)return ${volumeOptions}.volume=200;` replace: "$&>200?200:$&"
}, },
{ {
match: /(?<=Object\.entries\(\i\.localMutes\).+?volume:).+?(?=,)/, match: /(?<=Object\.entries\(\i\.localMutes\).+?volume:).+?(?=,)/,
@ -74,10 +74,10 @@ export default definePlugin({
find: '.displayName="MediaEngineStore"', find: '.displayName="MediaEngineStore"',
replacement: [ replacement: [
{ {
match: /(?<=\.settings\.audioContextSettings.+?)(\i\[\i\])=(\i\.volume)(.+?setLocalVolume\(\i,).+?\)/, match: /(\.settings\.audioContextSettings.+?)(\i\[\i\])=(\i\.volume)(.+?setLocalVolume\(\i,).+?\)/,
replace: (_, localVolume, syncVolume, rest) => "" replace: (_, rest1, localVolume, syncVolume, rest2) => rest1
+ `(${localVolume}>200?void 0:${localVolume}=${syncVolume})` + `(${localVolume}>200?void 0:${localVolume}=${syncVolume})`
+ rest + rest2
+ `${localVolume}??${syncVolume})` + `${localVolume}??${syncVolume})`
} }
] ]

View File

@ -16,9 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { PatchReplacement } from "./types"; import { PatchReplacement, ReplaceFn } from "./types";
export type ReplaceFn = (match: string, ...groups: string[]) => string;
export function canonicalizeMatch(match: RegExp | string) { export function canonicalizeMatch(match: RegExp | string) {
if (typeof match === "string") return match; if (typeof match === "string") return match;

View File

@ -19,13 +19,13 @@
import { Command } from "@api/Commands"; import { Command } from "@api/Commands";
import { Promisable } from "type-fest"; import { Promisable } from "type-fest";
import type { ReplaceFn } from "./patches";
// exists to export default definePlugin({...}) // exists to export default definePlugin({...})
export default function definePlugin<P extends PluginDef>(p: P & Record<string, any>) { export default function definePlugin<P extends PluginDef>(p: P & Record<string, any>) {
return p; return p;
} }
export type ReplaceFn = (match: string, ...groups: string[]) => string;
export interface PatchReplacement { export interface PatchReplacement {
match: string | RegExp; match: string | RegExp;
replace: string | ReplaceFn; replace: string | ReplaceFn;