Merge branch 'main' into plugin/memberListActivities

This commit is contained in:
D3SOX 2024-04-02 19:43:22 +02:00
commit 3cfefad3f5
No known key found for this signature in database
GPG key ID: 39EC1673FC37B048
127 changed files with 2462 additions and 873 deletions

View file

@ -26,6 +26,7 @@ import { debounce } from "../src/utils";
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
import { getTheme, Theme } from "../src/utils/discord";
import { getThemeInfo } from "../src/main/themes";
import { Settings } from "../src/Vencord";
// Discord deletes this so need to store in variable
const { localStorage } = window;
@ -96,8 +97,15 @@ window.VencordNative = {
},
settings: {
get: () => localStorage.getItem("VencordSettings") || "{}",
set: async (s: string) => localStorage.setItem("VencordSettings", s),
get: () => {
try {
return JSON.parse(localStorage.getItem("VencordSettings") || "{}");
} catch (e) {
console.error("Failed to parse settings from localStorage: ", e);
return {};
}
},
set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)),
getSettingsDir: async () => "LocalStorage"
},

View file

@ -95,5 +95,3 @@ Simply run:
```shell
pnpm uninject
```
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.7.0",
"version": "1.7.4",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {

View file

@ -67,7 +67,8 @@ const IGNORED_DISCORD_ERRORS = [
"Unable to process domain list delta: Client revision number is null",
"Downloading the full bad domains file",
/\[GatewaySocket\].{0,110}Cannot access '/,
"search for 'name' in undefined"
"search for 'name' in undefined",
"Attempting to set fast connect zstd when unsupported"
] as Array<string | RegExp>;
function toCodeBlock(s: string) {

View file

@ -4,11 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { IpcEvents } from "@utils/IpcEvents";
import { PluginIpcMappings } from "@main/ipcPlugins";
import type { UserThemeHeader } from "@main/themes";
import { IpcEvents } from "@shared/IpcEvents";
import { IpcRes } from "@utils/types";
import type { Settings } from "api/Settings";
import { ipcRenderer } from "electron";
import { PluginIpcMappings } from "main/ipcPlugins";
import type { UserThemeHeader } from "main/themes";
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
@ -46,8 +47,8 @@ export default {
},
settings: {
get: () => sendSync<string>(IpcEvents.GET_SETTINGS),
set: (settings: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings),
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
},

View file

@ -17,22 +17,20 @@
*/
import { Logger } from "@utils/Logger";
import { Menu, React } from "@webpack/common";
import type { ReactElement } from "react";
type ContextMenuPatchCallbackReturn = (() => void) | void;
/**
* @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
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => void;
/**
* @param navId The navId of the context menu being patched
* @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
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => void;
const ContextMenuLogger = new Logger("ContextMenu");
@ -93,14 +91,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
* @param id The id of the child. If an array is specified, all ids will be tried
* @param children The context menu children
*/
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>, _itemsArray?: Array<ReactElement | null>): Array<ReactElement | null> | null {
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>): Array<ReactElement | null> | null {
for (const child of children) {
if (child == null) continue;
if (Array.isArray(child)) {
const found = findGroupChildrenByChildId(id, child);
if (found !== null) return found;
}
if (
(Array.isArray(id) && id.some(id => child.props?.id === id))
|| child.props?.id === id
) return _itemsArray ?? null;
) return children;
let nextChildren = child.props?.children;
if (nextChildren) {
@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
child.props.children = nextChildren;
}
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
const found = findGroupChildrenByChildId(id, nextChildren);
if (found !== null) return found;
}
}
@ -126,9 +129,12 @@ interface ContextMenuProps {
onClose: (callback: (...args: Array<any>) => any) => void;
}
const patchedMenus = new WeakSet();
export function _usePatchContextMenu(props: ContextMenuProps) {
props = {
...props,
children: cloneMenuChildren(props.children),
};
export function _patchContextMenu(props: ContextMenuProps) {
props.contextMenuApiArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId);
@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) {
if (contextMenuPatches) {
for (const patch of contextMenuPatches) {
try {
const callback = patch(props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
patch(props.children, ...props.contextMenuApiArguments);
} catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
}
@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) {
try {
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
patch(props.navId, props.children, ...props.contextMenuApiArguments);
} catch (err) {
ContextMenuLogger.error("Global patch errored,", err);
}
}
patchedMenus.add(props);
return props;
}
function cloneMenuChildren(obj: ReactElement | Array<ReactElement | null> | null) {
if (Array.isArray(obj)) {
return obj.map(cloneMenuChildren);
}
if (React.isValidElement(obj)) {
obj = React.cloneElement(obj);
if (
obj?.props?.children &&
(obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null)
) {
obj.props.children = cloneMenuChildren(obj.props.children);
}
}
return obj;
}

View file

@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { debounce } from "@utils/debounce";
import { debounce } from "@shared/debounce";
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
import { localStorage } from "@utils/localStorage";
import { Logger } from "@utils/Logger";
import { mergeDefaults } from "@utils/misc";
@ -52,7 +53,6 @@ export interface Settings {
| "under-page"
| "window"
| undefined;
macosTranslucency: boolean | undefined;
disableMinSize: boolean;
winNativeTitleBar: boolean;
plugins: {
@ -88,8 +88,6 @@ const DefaultSettings: Settings = {
frameless: false,
transparent: false,
winCtrlQ: false,
// Replaced by macosVibrancyStyle
macosTranslucency: undefined,
macosVibrancyStyle: undefined,
disableMinSize: false,
winNativeTitleBar: false,
@ -110,13 +108,8 @@ const DefaultSettings: Settings = {
}
};
try {
var settings = JSON.parse(VencordNative.settings.get()) as Settings;
mergeDefaults(settings, DefaultSettings);
} catch (err) {
var settings = mergeDefaults({} as Settings, DefaultSettings);
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
}
const settings = VencordNative.settings.get();
mergeDefaults(settings, DefaultSettings);
const saveSettingsOnFrequentAction = debounce(async () => {
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
@ -125,76 +118,52 @@ const saveSettingsOnFrequentAction = debounce(async () => {
}
}, 60_000);
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array<string>; };
const subscriptions = new Set<SubscriptionCallback>();
const proxyCache = {} as Record<string, any>;
export const SettingsStore = new SettingsStoreClass(settings, {
readOnly: true,
getDefaultValue({
target,
key,
path
}) {
const v = target[key];
if (!plugins) return v; // plugins not initialised yet. this means this path was reached by being called on the top level
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
function makeProxy(settings: any, root = settings, path = ""): Settings {
return proxyCache[path] ??= new Proxy(settings, {
get(target, p: string) {
const v = target[p];
if (path === "plugins" && key in plugins)
return target[key] = {
enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
};
// using "in" is important in the following cases to properly handle falsy or nullish values
if (!(p in target)) {
// Return empty for plugins with no settings
if (path === "plugins" && p in plugins)
return target[p] = makeProxy({
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
}, root, `plugins.${p}`);
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
// the default value.
if (path.startsWith("plugins.")) {
const plugin = path.slice("plugins.".length);
if (plugin in plugins) {
const setting = plugins[plugin].options?.[key];
if (!setting) return v;
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
// the default value.
if (path.startsWith("plugins.")) {
const plugin = path.slice("plugins.".length);
if (plugin in plugins) {
const setting = plugins[plugin].options?.[p];
if (!setting) return v;
if ("default" in setting)
// normal setting with a default value
return (target[p] = setting.default);
if (setting.type === OptionType.SELECT) {
const def = setting.options.find(o => o.default);
if (def)
target[p] = def.value;
return def?.value;
}
}
}
return v;
}
if ("default" in setting)
// normal setting with a default value
return (target[key] = setting.default);
// Recursively proxy Objects with the updated property path
if (typeof v === "object" && !Array.isArray(v) && v !== null)
return makeProxy(v, root, `${path}${path && "."}${p}`);
// primitive or similar, no need to proxy further
return v;
},
set(target, p: string, v) {
// avoid unnecessary updates to React Components and other listeners
if (target[p] === v) return true;
target[p] = v;
// Call any listeners that are listening to a setting of this path
const setPath = `${path}${path && "."}${p}`;
delete proxyCache[setPath];
for (const subscription of subscriptions) {
if (!subscription._paths || subscription._paths.includes(setPath)) {
subscription(v, setPath);
if (setting.type === OptionType.SELECT) {
const def = setting.options.find(o => o.default);
if (def)
target[key] = def.value;
return def?.value;
}
}
// And don't forget to persist the settings!
PlainSettings.cloud.settingsSyncVersion = Date.now();
localStorage.Vencord_settingsDirty = true;
saveSettingsOnFrequentAction();
VencordNative.settings.set(JSON.stringify(root, null, 4));
return true;
}
});
}
return v;
}
});
SettingsStore.addGlobalChangeListener((_, path) => {
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
localStorage.Vencord_settingsDirty = true;
saveSettingsOnFrequentAction();
VencordNative.settings.set(SettingsStore.plain, path);
});
/**
* Same as {@link Settings} but unproxied. You should treat this as readonly,
@ -210,7 +179,7 @@ export const PlainSettings = settings;
* the updated settings to disk.
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
*/
export const Settings = makeProxy(settings);
export const Settings = SettingsStore.store;
/**
* Settings hook for React components. Returns a smart settings
@ -223,43 +192,21 @@ export const Settings = makeProxy(settings);
export function useSettings(paths?: UseSettings<Settings>[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
const onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
: forceUpdate;
React.useEffect(() => {
subscriptions.add(onUpdate);
return () => void subscriptions.delete(onUpdate);
if (paths) {
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
} else {
SettingsStore.addGlobalChangeListener(forceUpdate);
return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
}
}, []);
return Settings;
}
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
type ResolvePropDeep<T, P> = P extends "" ? T :
P extends `${infer Pre}.${infer Suf}` ?
Pre extends keyof T ? ResolvePropDeep<T[Pre], Suf> : never : P extends keyof T ? T[P] : never;
/**
* Add a settings listener that will be invoked whenever the desired setting is updated
* @param path Path to the setting that you want to watch, for example "plugins.Unindent.enabled" will fire your callback
* whenever Unindent is toggled. Pass an empty string to get notified for all changes
* @param onUpdate Callback function whenever a setting matching path is updated. It gets passed the new value and the path
* to the updated setting. This path will be the same as your path argument, unless it was an empty string.
*
* @example addSettingsListener("", (newValue, path) => console.log(`${path} is now ${newValue}`))
* addSettingsListener("plugins.Unindent.enabled", v => console.log("Unindent is now", v ? "enabled" : "disabled"))
*/
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
if (path)
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
subscriptions.add(onUpdate);
return SettingsStore.store;
}
export function migratePluginSettings(name: string, ...oldNames: string[]) {
const { plugins } = settings;
const { plugins } = SettingsStore.plain;
if (name in plugins) return;
for (const oldName of oldNames) {
@ -267,7 +214,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
plugins[name] = plugins[oldName];
delete plugins[oldName];
VencordNative.settings.set(JSON.stringify(settings, null, 4));
SettingsStore.markAsChanged();
break;
}
}

View file

@ -18,13 +18,13 @@
import { CheckedTextInput } from "@components/CheckedTextInput";
import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@utils/debounce";
import { debounce } from "@shared/debounce";
import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { makeCodeblock } from "@utils/text";
import { ReplaceFn } from "@utils/types";
import { Patch, ReplaceFn } from "@utils/types";
import { search } from "@webpack";
import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common";
import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
@ -218,6 +218,60 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
);
}
interface FullPatchInputProps {
setFind(v: string): void;
setMatch(v: string): void;
setReplacement(v: string | ReplaceFn): void;
}
function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputProps) {
const [fullPatch, setFullPatch] = React.useState<string>("");
const [fullPatchError, setFullPatchError] = React.useState<string>("");
function update() {
if (fullPatch === "") {
setFullPatchError("");
setFind("");
setMatch("");
setReplacement("");
return;
}
try {
const parsed = (0, eval)(`(${fullPatch})`) as Patch;
if (!parsed.find) throw new Error("No 'find' field");
if (!parsed.replacement) throw new Error("No 'replacement' field");
if (parsed.replacement instanceof Array) {
if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
parsed.replacement = {
match: parsed.replacement[0].match,
replace: parsed.replacement[0].replace
};
}
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
setFind(parsed.find);
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
setReplacement(parsed.replacement.replace);
setFullPatchError("");
} catch (e) {
setFullPatchError((e as Error).message);
}
}
return <>
<Forms.FormText>Paste your full JSON patch here to fill out the fields</Forms.FormText>
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
</>;
}
function PatchHelper() {
const [find, setFind] = React.useState<string>("");
const [match, setMatch] = React.useState<string>("");
@ -260,6 +314,13 @@ function PatchHelper() {
return (
<SettingsTab title="Patch Helper">
<Forms.FormTitle>full patch</Forms.FormTitle>
<FullPatchInput
setFind={onFindChange}
setMatch={onMatchChange}
setReplacement={setReplacement}
/>
<Forms.FormTitle>find</Forms.FormTitle>
<TextInput
type="text"

View file

@ -22,6 +22,7 @@ import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
import { Link } from "@components/Link";
import PluginModal from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
@ -30,7 +31,6 @@ import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack";
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import { UserThemeHeader } from "main/themes";
import type { ComponentType, Ref, SyntheticEvent } from "react";
import { AddonCard } from "./AddonCard";

View file

@ -50,14 +50,6 @@ function VencordSettings() {
const isMac = navigator.platform.toLowerCase().startsWith("mac");
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
// One-time migration of the old setting to the new one if necessary.
React.useEffect(() => {
if (settings.macosTranslucency === true && !settings.macosVibrancyStyle) {
settings.macosVibrancyStyle = "sidebar";
settings.macosTranslucency = undefined;
}
}, []);
const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>;
title: string;
@ -164,7 +156,7 @@ function VencordSettings() {
options={[
// Sorted from most opaque to most transparent
{
label: "No vibrancy", default: !settings.macosTranslucency, value: undefined
label: "No vibrancy", value: undefined
},
{
label: "Under Page (window tinting)",
@ -191,9 +183,8 @@ function VencordSettings() {
value: "header"
},
{
label: "Sidebar (old value for transparent windows)",
value: "sidebar",
default: settings.macosTranslucency
label: "Sidebar",
value: "sidebar"
},
{
label: "Tooltip",

View file

@ -19,7 +19,8 @@
import { app, protocol, session } from "electron";
import { join } from "path";
import { ensureSafePath, getSettings } from "./ipcMain";
import { ensureSafePath } from "./ipcMain";
import { RendererSettings } from "./settings";
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
import { installExt } from "./utils/extensions";
@ -55,7 +56,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
});
try {
if (getSettings().enableReactDevtools)
if (RendererSettings.store.enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));

View file

@ -18,22 +18,21 @@
import "./updater";
import "./ipcPlugins";
import "./settings";
import { debounce } from "@utils/debounce";
import { IpcEvents } from "@utils/IpcEvents";
import { Queue } from "@utils/Queue";
import { debounce } from "@shared/debounce";
import { IpcEvents } from "@shared/IpcEvents";
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
import { open, readdir, readFile, writeFile } from "fs/promises";
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
import { open, readdir, readFile } from "fs/promises";
import { join, normalize } from "path";
import monacoHtml from "~fileContent/monacoWin.html;base64";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
import { makeLinksOpenExternally } from "./utils/externalLinks";
mkdirSync(SETTINGS_DIR, { recursive: true });
mkdirSync(THEMES_DIR, { recursive: true });
export function ensureSafePath(basePath: string, path: string) {
@ -71,22 +70,6 @@ function getThemeData(fileName: string) {
return readFile(safePath, "utf-8");
}
export function readSettings() {
try {
return readFileSync(SETTINGS_FILE, "utf-8");
} catch {
return "{}";
}
}
export function getSettings(): typeof import("@api/Settings").Settings {
try {
return JSON.parse(readSettings());
} catch {
return {} as any;
}
}
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
@ -101,12 +84,10 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
shell.openExternal(url);
});
const cssWriteQueue = new Queue();
const settingsWriteQueue = new Queue();
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
writeFileSync(QUICKCSS_PATH, css)
);
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
@ -117,25 +98,25 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
}));
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
});
export function initIpc(mainWindow: BrowserWindow) {
let quickCssWatcher: FSWatcher | undefined;
open(QUICKCSS_PATH, "a+").then(fd => {
fd.close();
watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
}, 50));
});
}).catch(() => { });
watch(THEMES_DIR, { persistent: false }, debounce(() => {
const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => {
mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0);
}));
mainWindow.once("closed", () => {
quickCssWatcher?.close();
themesWatcher.close();
});
}
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { IpcEvents } from "@utils/IpcEvents";
import { IpcEvents } from "@shared/IpcEvents";
import { ipcMain } from "electron";
import PluginNatives from "~pluginNatives";

View file

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { onceDefined } from "@utils/onceDefined";
import { onceDefined } from "@shared/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import { dirname, join } from "path";
import { getSettings, initIpc } from "./ipcMain";
import { initIpc } from "./ipcMain";
import { RendererSettings } from "./settings";
import { IS_VANILLA } from "./utils/constants";
console.log("[Vencord] Starting up...");
@ -41,8 +42,7 @@ require.main!.filename = join(asarPath, discordPkg.main);
app.setAppPath(asarPath);
if (!IS_VANILLA) {
const settings = getSettings();
const settings = RendererSettings.store;
// Repatch after host updates on Windows
if (process.platform === "win32") {
require("./patchWin32Updater");
@ -84,13 +84,11 @@ if (!IS_VANILLA) {
options.backgroundColor = "#00000000";
}
const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency);
const needsVibrancy = process.platform === "darwin" && settings.macosVibrancyStyle;
if (needsVibrancy) {
options.backgroundColor = "#00000000";
if (settings.macosTranslucency) {
options.vibrancy = "sidebar";
} else if (settings.macosVibrancyStyle) {
if (settings.macosVibrancyStyle) {
options.vibrancy = settings.macosVibrancyStyle;
}
}

53
src/main/settings.ts Normal file
View file

@ -0,0 +1,53 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { Settings } from "@api/Settings";
import { IpcEvents } from "@shared/IpcEvents";
import { SettingsStore } from "@shared/SettingsStore";
import { ipcMain } from "electron";
import { mkdirSync, readFileSync, writeFileSync } from "fs";
import { NATIVE_SETTINGS_FILE, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
mkdirSync(SETTINGS_DIR, { recursive: true });
function readSettings<T = object>(name: string, file: string): Partial<T> {
try {
return JSON.parse(readFileSync(file, "utf-8"));
} catch (err: any) {
if (err?.code !== "ENOENT")
console.error(`Failed to read ${name} settings`, err);
return {};
}
}
export const RendererSettings = new SettingsStore(readSettings<Settings>("renderer", SETTINGS_FILE));
RendererSettings.addGlobalChangeListener(() => {
try {
writeFileSync(SETTINGS_FILE, JSON.stringify(RendererSettings.plain, null, 4));
} catch (e) {
console.error("Failed to write renderer settings", e);
}
});
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain);
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {
RendererSettings.setData(data, pathToNotify);
});
export const NativeSettings = new SettingsStore(readSettings("native", NATIVE_SETTINGS_FILE));
NativeSettings.addGlobalChangeListener(() => {
try {
writeFileSync(NATIVE_SETTINGS_FILE, JSON.stringify(NativeSettings.plain, null, 4));
} catch (e) {
console.error("Failed to write native settings", e);
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { IpcEvents } from "@utils/IpcEvents";
import { IpcEvents } from "@shared/IpcEvents";
import { execFile as cpExecFile } from "child_process";
import { ipcMain } from "electron";
import { join } from "path";
@ -49,9 +49,12 @@ async function getRepo() {
async function calculateGitChanges() {
await git("fetch");
const branch = await git("branch", "--show-current");
const branch = (await git("branch", "--show-current")).stdout.trim();
const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s");
const existsOnOrigin = (await git("ls-remote", "origin", branch)).stdout.length > 0;
if (!existsOnOrigin) return [];
const res = await git("log", `HEAD...origin/${branch}`, "--pretty=format:%an/%h/%s");
const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => {

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { VENCORD_USER_AGENT } from "@utils/constants";
import { IpcEvents } from "@utils/IpcEvents";
import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron";
import { writeFile } from "fs/promises";
import { join } from "path";
@ -53,7 +53,7 @@ async function calculateGitChanges() {
// github api only sends the long sha
hash: c.sha.slice(0, 7),
author: c.author.login,
message: c.commit.message
message: c.commit.message.substring(c.commit.message.indexOf("\n") + 1)
}));
}

View file

@ -28,6 +28,7 @@ export const SETTINGS_DIR = join(DATA_DIR, "settings");
export const THEMES_DIR = join(DATA_DIR, "themes");
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
export const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, "native-settings.json");
export const ALLOWED_PROTOCOLS = [
"https:",
"http:",

View file

@ -13,10 +13,10 @@ export default definePlugin({
authors: [Devs.Ven],
patches: [{
find: 'location:"ChannelTextAreaButtons"',
find: '"sticker")',
replacement: {
match: /if\(!\i\.isMobile\)\{(?=.+?&&(\i)\.push\(.{0,50}"gift")/,
replace: "$&Vencord.Api.ChatButtons._injectButtons($1,arguments[0]);"
match: /!\i\.isMobile(?=.+?(\i)\.push\(.{0,50}"gift")/,
replace: "$& &&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)"
}
}]
});

View file

@ -22,15 +22,15 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "ContextMenuAPI",
description: "API for adding/removing items to/from context menus.",
authors: [Devs.Nuckyz, Devs.Ven],
authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi],
required: true,
patches: [
{
find: "♫ (つ。◕‿‿◕。)つ ♪",
replacement: {
match: /let{navId:/,
replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&"
match: /(?=let{navId:)(?<=function \i\((\i)\).+?)/,
replace: "$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);"
}
},
{

View file

@ -31,7 +31,7 @@ export default definePlugin({
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
replace: "$&vencordProps=$1,"
}, {
match: /decorators:.{0,100}?children:\[/,
match: /\.Messages\.GUILD_OWNER(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
}
]

View file

@ -35,7 +35,7 @@ export default definePlugin({
}
},
{
find: ".handleSendMessage=",
find: ".handleSendMessage",
replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)

View file

@ -26,7 +26,7 @@ export default definePlugin({
required: true,
patches: [
{
find: 'displayName="NoticeStore"',
find: '"NoticeStore"',
replacement: [
{
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,

View file

@ -16,11 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { React, SettingsRouter } from "@webpack/common";
import { React } from "@webpack/common";
import gitHash from "~git-hash";
@ -30,23 +29,6 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Megu],
required: true,
start() {
// The settings shortcuts in the user settings cog context menu
// read the elements from a hardcoded map which for obvious reason
// doesn't contain our sections. This patches the actions of our
// sections to manually use SettingsRouter (which only works on desktop
// but the context menu is usually not available on mobile anyway)
addContextMenuPatch("user-settings-cog", children => () => {
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any;
section?.forEach(c => {
const id = c?.props?.id;
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
c.props.action = () => SettingsRouter.open(id);
}
});
});
},
patches: [{
find: ".versionHash",
replacement: [
@ -75,6 +57,12 @@ export default definePlugin({
},
replace: "...$self.makeSettingsCategories($1),$&"
}
}, {
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
replace: "$2.default.open($1);return;"
}
}],
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],

View file

@ -16,27 +16,46 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
domain: {
type: OptionType.BOOLEAN,
default: true,
description: "Remove the untrusted domain popup when opening links",
restartNeeded: true
},
file: {
type: OptionType.BOOLEAN,
default: true,
description: "Remove the 'Potentially Dangerous Download' popup when opening links",
restartNeeded: true
}
});
export default definePlugin({
name: "AlwaysTrust",
description: "Removes the annoying untrusted domain and suspicious file popup",
authors: [Devs.zt],
authors: [Devs.zt, Devs.Trwy],
patches: [
{
find: ".displayName=\"MaskedLinkStore\"",
find: '="MaskedLinkStore",',
replacement: {
match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/,
replace: "return true"
}
},
predicate: () => settings.store.domain
},
{
find: "isSuspiciousDownload:",
replacement: {
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
replace: "$&return null;"
}
},
predicate: () => settings.store.file
}
]
],
settings
});

View file

@ -67,7 +67,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "AnonymiseFileNames",
authors: [Devs.obscurity],
authors: [Devs.fawn],
description: "Anonymise uploaded file names",
patches: [
{
@ -78,6 +78,13 @@ export default definePlugin({
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
},
},
{
find: "message.attachments",
replacement: {
match: /(\i.uploadFiles\((\i),)/,
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
}
},
{
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
replacement: {

View file

@ -26,7 +26,7 @@ export default definePlugin({
"Change GIF alt text from simply being 'GIF' to containing the gif tags / filename",
patches: [
{
find: "onCloseImage=",
find: '"onCloseImage",',
replacement: {
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
replace:

View file

@ -15,8 +15,8 @@ export default definePlugin({
{
find: ".GIFPickerResultTypes.SEARCH",
replacement: [{
match: "this.state={resultType:null}",
replace: 'this.state={resultType:"Favorites"}'
match: /(?<="state",{resultType:)null/,
replace: '"Favorites"'
}]
}
]

View file

@ -18,6 +18,7 @@
import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
@ -39,8 +40,12 @@ export default definePlugin({
match: /hideNote:.+?(?=([,}].*?\)))/g,
replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) return "hideNote:!0";
return m;
if (destructuringMatch) {
const defaultValueMatch = m.match(canonicalizeMatch(/hideNote:(\i)=!?\d/));
return defaultValueMatch ? `hideNote:${defaultValueMatch[1]}=!0` : m;
}
return "hideNote:!0";
}
}
},
@ -52,10 +57,10 @@ export default definePlugin({
}
},
{
find: ".Messages.NOTE}",
find: ".popularApplicationCommandIds,",
replacement: {
match: /(?<=return \i\?)null(?=:\(0,\i\.jsxs)/,
replace: "$self.patchPadding(arguments[0])"
match: /lastSection:(!?\i)}\),/,
replace: "$&$self.patchPadding($1),"
}
}
],
@ -75,8 +80,8 @@ export default definePlugin({
}
},
patchPadding(e: any) {
if (!e.lastSection) return;
patchPadding(lastSection: any) {
if (!lastSection) return;
return (
<div className={UserPopoutSectionCssClasses.lastSection}></div>
);

View file

@ -0,0 +1,6 @@
# BetterRoleContext
Adds options to copy role color and edit role when right clicking roles in the user profile
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326)

View file

@ -0,0 +1,81 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import { getCurrentGuild } from "@utils/discord";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
function PencilIcon() {
return (
<svg
role="img"
width="18"
height="18"
fill="none"
viewBox="0 0 24 24"
>
<path fill="currentColor" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" />
</svg>
);
}
function AppearanceIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M 12,0 C 5.3733333,0 0,5.3733333 0,12 c 0,6.626667 5.3733333,12 12,12 1.106667,0 2,-0.893333 2,-2 0,-0.52 -0.2,-0.986667 -0.52,-1.346667 -0.306667,-0.346666 -0.506667,-0.813333 -0.506667,-1.32 0,-1.106666 0.893334,-2 2,-2 h 2.36 C 21.013333,17.333333 24,14.346667 24,10.666667 24,4.7733333 18.626667,0 12,0 Z M 4.6666667,12 c -1.1066667,0 -2,-0.893333 -2,-2 0,-1.1066667 0.8933333,-2 2,-2 1.1066666,0 2,0.8933333 2,2 0,1.106667 -0.8933334,2 -2,2 z M 8.666667,6.6666667 c -1.106667,0 -2.0000003,-0.8933334 -2.0000003,-2 0,-1.1066667 0.8933333,-2 2.0000003,-2 1.106666,0 2,0.8933333 2,2 0,1.1066666 -0.893334,2 -2,2 z m 6.666666,0 c -1.106666,0 -2,-0.8933334 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.1066666 -0.893333,2 -2,2 z m 4,5.3333333 c -1.106666,0 -2,-0.893333 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.106667 -0.893333,2 -2,2 z" />
</svg>
);
}
export default definePlugin({
name: "BetterRoleContext",
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
authors: [Devs.Ven],
start() {
// DeveloperMode needs to be enabled for the context menu to be shown
TextAndImagesSettingsStores.DeveloperMode.updateSetting(true);
},
contextMenus: {
"dev-context"(children, { id }: { id: string; }) {
const guild = getCurrentGuild();
if (!guild) return;
const role = GuildStore.getRole(guild.id, id);
if (!role) return;
if (role.colorString) {
children.push(
<Menu.MenuItem
id="vc-copy-role-color"
label="Copy Role Color"
action={() => Clipboard.copy(role.colorString!)}
icon={AppearanceIcon}
/>
);
}
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
}
}
});

View file

@ -0,0 +1,9 @@
# BetterSettings
Improves Discord's Settings via multiple (toggleable) changes:
- makes opening settings much faster
- removes the scuffed transition animation
- organises the settings cog context menu into categories
![](https://github.com/Vendicated/Vencord/assets/45497981/e8d67a95-3909-4be5-8281-8cf9d2f1c30e)

View file

@ -0,0 +1,177 @@
/*
* 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 { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
import type { HTMLAttributes, ReactElement } from "react";
type SettingsEntry = { section: string, label: string; };
const cl = classNameFactory("");
const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers");
const settings = definePluginSettings({
disableFade: {
description: "Disable the crossfade animation",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
},
organizeMenu: {
description: "Organizes the settings cog context menu into categories",
type: OptionType.BOOLEAN,
default: true
},
eagerLoad: {
description: "Removes the loading delay when opening the menu for the first time",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
}
});
interface LayerProps extends HTMLAttributes<HTMLDivElement> {
mode: "SHOWN" | "HIDDEN";
baseLayer?: boolean;
}
function Layer({ mode, baseLayer = false, ...props }: LayerProps) {
const hidden = mode === "HIDDEN";
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => () => {
ComponentDispatch.dispatch("LAYER_POP_START");
ComponentDispatch.dispatch("LAYER_POP_COMPLETE");
}, []);
const node = (
<div
ref={containerRef}
aria-hidden={hidden}
className={cl({
[Classes.layer]: true,
[Classes.baseLayer]: baseLayer,
"stop-animations": hidden
})}
style={{ opacity: hidden ? 0 : undefined }}
{...props}
/>
);
return baseLayer
? node
: <FocusLock containerRef={containerRef}>{node}</FocusLock>;
}
export default definePlugin({
name: "BetterSettings",
description: "Enhances your settings-menu-opening experience",
authors: [Devs.Kyuuhachi],
settings,
patches: [
{
find: "this.renderArtisanalHack()",
replacement: [
{ // Fade in on layer
match: /(?<=\((\i),"contextType",\i\.AccessibilityPreferencesContext\);)/,
replace: "$1=$self.Layer;",
predicate: () => settings.store.disableFade
},
{ // Lazy-load contents
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
replace: "$&,_:$1",
predicate: () => settings.store.eagerLoad
}
]
},
{ // For some reason standardSidebarView also has a small fade-in
find: "DefaultCustomContentScroller:function()",
replacement: [
{
match: /\(0,\i\.useTransition\)\((\i)/,
replace: "(_cb=>_cb(void 0,$1))||$&"
},
{
match: /\i\.animated\.div/,
replace: '"div"'
}
],
predicate: () => settings.store.disableFade
},
{ // Load menu TOC eagerly
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: {
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
replace: "(async ()=>$2)(),"
},
predicate: () => settings.store.eagerLoad
},
{ // Settings cog context menu
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$self.wrapMenu($&)"
}
}
],
Layer(props: LayerProps) {
return (
<ErrorBoundary fallback={() => props.children as any}>
<Layer {...props} />
</ErrorBoundary>
);
},
wrapMenu(list: SettingsEntry[]) {
if (!settings.store.organizeMenu) return list;
const items = [{ label: null as string | null, items: [] as SettingsEntry[] }];
for (const item of list) {
if (item.section === "HEADER") {
items.push({ label: item.label, items: [] });
} else if (item.section === "DIVIDER") {
items.push({ label: i18n.Messages.OTHER_OPTIONS, items: [] });
} else {
items.at(-1)!.items.push(item);
}
}
return {
filter(predicate: (item: SettingsEntry) => boolean) {
for (const category of items) {
category.items = category.items.filter(predicate);
}
return this;
},
map(render: (item: SettingsEntry) => ReactElement) {
return items
.filter(a => a.items.length > 0)
.map(({ label, items }) => {
const children = items.map(render);
if (label) {
return (
<Menu.MenuItem
id={label.replace(/\W/, "_")}
label={label}
children={children}
action={children[0].props.action}
/>);
} else {
return children;
}
});
}
};
}
});

View file

@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "BetterUploadButton",
authors: [Devs.obscurity, Devs.Ven],
authors: [Devs.fawn, Devs.Ven],
description: "Upload with a single click, open menu with right click",
patches: [
{

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { ScreenshareIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { openImageModal } from "@utils/discord";
@ -60,7 +60,7 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica
openImageModal(previewUrl);
};
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => () => {
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => {
const stream = ApplicationStreamingStore.getAnyStreamForUser(userId);
if (!stream) return;
@ -89,12 +89,8 @@ export default definePlugin({
name: "BiggerStreamPreview",
description: "This plugin allows you to enlarge stream previews",
authors: [Devs.phil],
start: () => {
addContextMenuPatch("user-context", userContextPatch);
addContextMenuPatch("stream-context", streamContextPatch);
},
stop: () => {
removeContextMenuPatch("user-context", userContextPatch);
removeContextMenuPatch("stream-context", streamContextPatch);
contextMenus: {
"user-context": userContextPatch,
"stream-context": streamContextPatch
}
});

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common";
import { Button, Forms, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
@ -200,8 +200,8 @@ function captureOne(str, regex) {
return (result === null) ? null : result[1];
}
function mapReject(arr, mapFunc, rejectFunc = _.isNull) {
return _.reject(arr.map(mapFunc), rejectFunc);
function mapReject(arr, mapFunc) {
return arr.map(mapFunc).filter(Boolean);
}
function updateColorVars(color: string) {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { LinkIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -29,7 +29,7 @@ interface UserContextProps {
user: User;
}
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
if (!user) return;
children.push(
@ -46,12 +46,7 @@ export default definePlugin({
name: "CopyUserURLs",
authors: [Devs.castdrian],
description: "Adds a 'Copy User URL' option to the user context menu.",
start() {
addContextMenuPatch("user-context", UserContextMenuPatch);
},
stop() {
removeContextMenuPatch("user-context", UserContextMenuPatch);
},
contextMenus: {
"user-context": UserContextMenuPatch
}
});

View file

@ -175,7 +175,7 @@ const settings = definePluginSettings({
},
startTime: {
type: OptionType.NUMBER,
description: "Start timestamp (only for custom timestamp mode)",
description: "Start timestamp in milisecond (only for custom timestamp mode)",
onChange: onChange,
disabled: isTimestampDisabled,
isValid: (value: number) => {
@ -185,7 +185,7 @@ const settings = definePluginSettings({
},
endTime: {
type: OptionType.NUMBER,
description: "End timestamp (only for custom timestamp mode)",
description: "End timestamp in milisecond (only for custom timestamp mode)",
onChange: onChange,
disabled: isTimestampDisabled,
isValid: (value: number) => {
@ -313,12 +313,12 @@ async function createActivity(): Promise<Activity | undefined> {
switch (settings.store.timestampMode) {
case TimestampMode.NOW:
activity.timestamps = {
start: Math.floor(Date.now() / 1000)
start: Date.now()
};
break;
case TimestampMode.TIME:
activity.timestamps = {
start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds()
start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000
};
break;
case TimestampMode.CUSTOM:

View file

@ -43,13 +43,13 @@ export default definePlugin({
{
find: "DefaultCustomizationSections",
replacement: {
match: /(?<={user:\i},"decoration"\),)/,
match: /(?<=USER_SETTINGS_AVATAR_DECORATION},"decoration"\),)/,
replace: "$self.DecorSection(),"
}
},
// Decoration modal module
{
find: ".decorationGridItem",
find: ".decorationGridItem,",
replacement: [
{
match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/,
@ -61,8 +61,8 @@ export default definePlugin({
},
// Remove NEW label from decor avatar decorations
{
match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i;if\()(?<=avatarDecoration:(\i).+?)/,
replace: "$1.skuId===$self.SKU_ID||"
match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i)(?<=avatarDecoration:(\i).+?)/,
replace: "||$1.skuId===$self.SKU_ID"
}
]
},
@ -131,9 +131,10 @@ export default definePlugin({
getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {
// Only Decor avatar decorations have this SKU ID
if (avatarDecoration?.skuId === SKU_ID) {
const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`);
url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString());
return url.toString();
const parts = avatarDecoration.asset.split("_");
// Remove a_ prefix if it's animated and animation is disabled
if (isAnimatedAvatarDecoration(avatarDecoration.asset) && !canAnimate) parts.shift();
return `${CDN_URL}/${parts.join("_")}.png`;
} else if (avatarDecoration?.skuId === RAW_SKU_ID) {
return avatarDecoration.asset;
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { debounce } from "@utils/debounce";
import { debounce } from "@shared/debounce";
import { proxyLazy } from "@utils/lazy";
import { useEffect, useState, zustandCreate } from "@webpack/common";
import { User } from "discord-types/general";

View file

@ -29,7 +29,7 @@ export default definePlugin({
{
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
replacement: {
match: /,?(?=this\.idleTimeout=new \i\.Timeout)/,
match: /,?(?=\i\(this,"idleTimeout",new \i\.Timeout\))/,
replace: ";return;"
}
},

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
@ -56,7 +56,7 @@ function getUrl(data: Data) {
if (data.t === "Emoji")
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`;
return `${location.origin}/stickers/${data.id}.${StickerExt[data.format_type]}`;
return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExt[data.format_type]}`;
}
async function fetchSticker(id: string) {
@ -312,7 +312,7 @@ function isGifUrl(url: string) {
return new URL(url).pathname.endsWith(".gif");
}
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
if (!favoriteableId) return;
@ -341,7 +341,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
findGroupChildrenByChildId("copy-link", children)?.push(menuItem);
};
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => {
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
const { id, name, type } = props?.target?.dataset ?? {};
if (!id) return;
@ -363,14 +363,8 @@ export default definePlugin({
description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
tags: ["StickerCloner"],
authors: [Devs.Ven, Devs.Nuckyz],
start() {
addContextMenuPatch("message", messageContextMenuPatch);
addContextMenuPatch("expression-picker", expressionPickerPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
removeContextMenuPatch("expression-picker", expressionPickerPatch);
contextMenus: {
"message": messageContextMenuPatch,
"expression-picker": expressionPickerPatch
}
});

View file

@ -64,7 +64,7 @@ export default definePlugin({
}
},
{
find: ".isStaff=()",
find: '"isStaff",',
predicate: () => settings.store.enableIsStaff,
replacement: [
{

View file

@ -162,7 +162,7 @@ const settings = definePluginSettings({
default: true
},
hyperLinkText: {
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji name.",
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.",
type: OptionType.STRING,
default: "{{NAME}}"
}
@ -185,7 +185,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
dependencies: ["MessageEventsAPI"],
@ -277,7 +277,7 @@ export default definePlugin({
}
},
{
find: '.displayName="UserSettingsProtoStore"',
find: '"UserSettingsProtoStore"',
replacement: [
{
// Overwrite incoming connection settings proto with our local settings
@ -388,6 +388,14 @@ export default definePlugin({
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true"
}
},
// Make all Soundboard sounds available
{
find: 'type:"GUILD_SOUNDBOARD_SOUND_CREATE"',
replacement: {
match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g,
replace: "true"
}
}
],
@ -585,13 +593,15 @@ export default definePlugin({
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
children = this.clearEmptyArrayItems(children);
this.trimContent(children);
return children;
};
try {
return modifyChildren(lodash.cloneDeep(content));
const newContent = modifyChildren(lodash.cloneDeep(content));
this.trimContent(newContent);
return newContent;
} catch (err) {
new Logger("FakeNitro").error(err);
return content;
@ -791,8 +801,8 @@ export default definePlugin({
title: "Hold on!",
body: <div>
<Forms.FormText>
You are trying to send/edit a message that contains a FakeNitro emoji or sticker
, however you do not have permissions to embed links in the current channel.
You are trying to send/edit a message that contains a FakeNitro emoji or sticker,
however you do not have permissions to embed links in the current channel.
Are you sure you want to send this message? Your FakeNitro items will appear as a link only.
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
@ -864,7 +874,9 @@ export default definePlugin({
const url = new URL(link);
url.searchParams.set("name", sticker.name);
messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${sticker.name}](${url})` : url}`;
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", sticker.name);
messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`;
extra.stickers!.length = 0;
}
}

View file

@ -200,7 +200,14 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
export function getTargetString(urlStr: string) {
const url = new URL(urlStr);
let url: URL;
try {
url = new URL(urlStr);
} catch (err) {
// Can't resolve URL, return as-is
return urlStr;
}
switch (settings.store.searchOption) {
case "url":
return url.href;

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { RendererSettings } from "@main/settings";
import { app } from "electron";
import { getSettings } from "main/ipcMain";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
const settings = getSettings().plugins?.FixSpotifyEmbeds;
const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds;
if (!settings?.enabled) return;
frame.executeJavaScript(`

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { RendererSettings } from "@main/settings";
import { app } from "electron";
import { getSettings } from "main/ipcMain";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
if (frame.url.startsWith("https://www.youtube.com/")) {
const settings = getSettings().plugins?.FixYoutubeEmbeds;
const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds;
if (!settings?.enabled) return;
frame.executeJavaScript(`

View file

@ -0,0 +1,5 @@
# FriendsSince
Shows when you became friends with someone in the user popout
![](https://github.com/Vendicated/Vencord/assets/45497981/bb258188-ab48-4c4d-9858-1e90ba41e926)

View file

@ -0,0 +1,60 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { React, RelationshipStore } from "@webpack/common";
const { Heading, Text } = findByPropsLazy("Heading", "Text");
const container = findByPropsLazy("memberSinceContainer");
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");
const locale = findByPropsLazy("getLocale");
const lastSection = findByPropsLazy("lastSection");
export default definePlugin({
name: "FriendsSince",
description: "Shows when you became friends with someone in the user popout",
authors: [Devs.Elvyra],
patches: [
{
find: ".AnalyticsSections.USER_PROFILE}",
replacement: {
match: /\i.default,\{userId:(\i.id).{0,30}}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })"
}
},
{
find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
replacement: {
match: /\i.default,\{userId:(\i)}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })"
}
}
],
friendsSince: ErrorBoundary.wrap(({ userId }: { userId: string; }) => {
const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null;
return (
<div className={lastSection.section}>
<Heading variant="eyebrow" className={clydeMoreInfo.title}>
Friends Since
</Heading>
<div className={container.memberSinceContainer}>
<Text variant="text-sm/normal" className={clydeMoreInfo.body}>
{getCreatedAtDate(friendsSince, locale.getLocale())}
</Text>
</div>
</div>
);
}, { noop: true })
});

View file

@ -29,10 +29,10 @@ export default definePlugin({
authors: [Devs.Ven],
patches: [{
find: ".handleSelectGIF=",
find: '"handleSelectGIF",',
replacement: {
match: /\.handleSelectGIF=(\i)=>\{/,
replace: ".handleSelectGIF=$1=>{if (!this.props.className) return $self.handleSelect($1);"
match: /"handleSelectGIF",(\i)=>\{/,
replace: '"handleSelectGIF",$1=>{if (!this.props.className) return $self.handleSelect($1);'
}
}],

View file

@ -5,12 +5,14 @@
*/
import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import { definePluginSettings, Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
import { StatusSettingsStores, Tooltip } from "webpack/common";
import { Button, Forms, showToast, StatusSettingsStores, TextInput, Toasts, Tooltip, useEffect, useState } from "webpack/common";
const enum ActivitiesTypes {
Game,
@ -69,7 +71,113 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
StatusSettingsStores.ShowCurrentGame.updateSetting(old => old);
}
const settings = definePluginSettings({}).withPrivateSettings<{
function ImportCustomRPCComponent() {
return (
<Flex flexDirection="column">
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the allowed list</Forms.FormText>
<div>
<Button
onClick={() => {
const id = Settings.plugins.CustomRPC?.appID as string | undefined;
if (!id) {
return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
}
const isAlreadyAdded = allowedIdsPushID?.(id);
if (isAlreadyAdded) {
showToast("CustomRPC application ID is already added.", Toasts.Type.FAILURE);
}
}}
>
Import CustomRPC ID
</Button>
</div>
</Flex>
);
}
let allowedIdsPushID: ((id: string) => boolean) | null = null;
function AllowedIdsComponent(props: { setValue: (value: string) => void; }) {
const [allowedIds, setAllowedIds] = useState<string>(settings.store.allowedIds ?? "");
allowedIdsPushID = (id: string) => {
const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean));
const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);
const ids = Array.from(currentIds).join(", ");
setAllowedIds(ids);
props.setValue(ids);
return isAlreadyAdded;
};
useEffect(() => () => {
allowedIdsPushID = null;
}, []);
function handleChange(newValue: string) {
setAllowedIds(newValue);
props.setValue(newValue);
}
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Allowed List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC)</Forms.FormText>
<TextInput
type="text"
value={allowedIds}
onChange={handleChange}
placeholder="235834946571337729, 343383572805058560"
/>
</Forms.FormSection>
);
}
const settings = definePluginSettings({
importCustomRPC: {
type: OptionType.COMPONENT,
description: "",
component: () => <ImportCustomRPCComponent />
},
allowedIds: {
type: OptionType.COMPONENT,
description: "",
default: "",
onChange(newValue: string) {
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
settings.store.allowedIds = Array.from(ids).join(", ");
},
component: props => <AllowedIdsComponent setValue={props.setValue} />
},
ignorePlaying: {
type: OptionType.BOOLEAN,
description: "Ignore all playing activities (These are usually game and RPC activities)",
default: false
},
ignoreStreaming: {
type: OptionType.BOOLEAN,
description: "Ignore all streaming activities",
default: false
},
ignoreListening: {
type: OptionType.BOOLEAN,
description: "Ignore all listening activities (These are usually spotify activities)",
default: false
},
ignoreWatching: {
type: OptionType.BOOLEAN,
description: "Ignore all watching activities",
default: false
},
ignoreCompeting: {
type: OptionType.BOOLEAN,
description: "Ignore all competing activities (These are normally special game activities)",
default: false
}
}).withPrivateSettings<{
ignoredActivities: IgnoredActivity[];
}>();
@ -77,19 +185,35 @@ function getIgnoredActivities() {
return settings.store.ignoredActivities ??= [];
}
function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.allowedIds.includes(id)) {
return false;
}
switch (type) {
case 0: return settings.store.ignorePlaying;
case 1: return settings.store.ignoreStreaming;
case 2: return settings.store.ignoreListening;
case 3: return settings.store.ignoreWatching;
case 5: return settings.store.ignoreCompeting;
}
return false;
}
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are ignored from the Registered Games and Activities tabs.",
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.",
settings,
patches: [
{
find: '.displayName="LocalActivityStore"',
find: '="LocalActivityStore",',
replacement: [
{
match: /HANG_STATUS.+?(?=!\i\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
match: /HANG_STATUS.+?(?=!\i\(\)\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
}
]
@ -141,13 +265,17 @@ export default definePlugin({
},
isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
if (props.type === 0 || props.type === 3) {
if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id);
else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) return !getIgnoredActivities().some(activity => activity.id === exePath);
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
if (props.application_id != null) {
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id);
} else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) {
return !getIgnoredActivities().some(activity => activity.id === exePath);
}
}
return true;
},

View file

@ -16,14 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { makeRange } from "@components/PluginSettings/components";
import { debounce } from "@shared/debounce";
import { Devs } from "@utils/constants";
import { debounce } from "@utils/debounce";
import definePlugin, { OptionType } from "@utils/types";
import { ContextMenuApi, Menu, React, ReactDOM } from "@webpack/common";
import { Menu, ReactDOM } from "@webpack/common";
import type { Root } from "react-dom/client";
import { Magnifier, MagnifierProps } from "./components/Magnifier";
@ -80,25 +80,25 @@ export const settings = definePluginSettings({
});
const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
const imageContextMenuPatch: NavContextMenuPatchCallback = children => {
const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]);
children.push(
<Menu.MenuGroup id="image-zoom">
<Menu.MenuCheckboxItem
id="vc-square"
label="Square Lens"
checked={settings.store.square}
checked={square}
action={() => {
settings.store.square = !settings.store.square;
ContextMenuApi.closeContextMenu();
settings.store.square = !square;
}}
/>
<Menu.MenuCheckboxItem
id="vc-nearest-neighbour"
label="Nearest Neighbour"
checked={settings.store.nearestNeighbour}
checked={nearestNeighbour}
action={() => {
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
ContextMenuApi.closeContextMenu();
settings.store.nearestNeighbour = !nearestNeighbour;
}}
/>
<Menu.MenuControlItem
@ -168,7 +168,7 @@ export default definePlugin({
},
{
find: "handleImageLoad=",
find: ".handleImageLoad)",
replacement: [
{
match: /placeholderVersion:\i,/,
@ -196,6 +196,9 @@ export default definePlugin({
],
settings,
contextMenus: {
"image-context": imageContextMenuPatch
},
// to stop from rendering twice /shrug
currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,
@ -245,7 +248,6 @@ export default definePlugin({
start() {
enableStyle(styles);
addContextMenuPatch("image-context", imageContextMenuPatch);
this.element = document.createElement("div");
this.element.classList.add("MagnifierContainer");
document.body.appendChild(this.element);
@ -256,6 +258,5 @@ export default definePlugin({
// so componenetWillUnMount gets called if Magnifier component is still alive
this.root && this.root.unmount();
this.element?.remove();
removeContextMenuPatch("image-context", imageContextMenuPatch);
}
});

View file

@ -17,6 +17,7 @@
*/
import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { Patch, Plugin, StartAt } from "@utils/types";
@ -119,7 +120,7 @@ export function startDependenciesRecursive(p: Plugin) {
}
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
const { name, commands, flux } = p;
const { name, commands, flux, contextMenus } = p;
if (p.start) {
logger.info("Starting plugin", name);
@ -154,11 +155,17 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
}
}
if (contextMenus) {
for (const navId in contextMenus) {
addContextMenuPatch(navId, contextMenus[navId]);
}
}
return true;
}, p => `startPlugin ${p.name}`);
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
const { name, commands, flux } = p;
const { name, commands, flux, contextMenus } = p;
if (p.stop) {
logger.info("Stopping plugin", name);
if (!p.started) {
@ -192,5 +199,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
}
}
if (contextMenus) {
for (const navId in contextMenus) {
removeContextMenuPatch(navId, contextMenus[navId]);
}
}
return true;
}, p => `stopPlugin ${p.name}`);

View file

@ -81,11 +81,11 @@ export default definePlugin({
find: ".LOADING_DID_YOU_KNOW}",
replacement: [
{
match: /\._loadingText=function\(\)\{/,
match: /"_loadingText",function\(\)\{/,
replace: "$&return $self.quote;",
},
{
match: /\._eventLoadingText=function\(\)\{/,
match: /"_eventLoadingText",function\(\)\{/,
replace: "$&return $self.quote;",
predicate: () => settings.store.replaceEvents
}

View file

@ -27,7 +27,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
const { groups } = useStateFromStores(
[ChannelMemberStore],
() => ChannelMemberStore.getProps(guildId, currentChannel.id)
() => ChannelMemberStore.getProps(guildId, currentChannel?.id)
);
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {

View file

@ -18,10 +18,11 @@
import "./style.css";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
import { FluxStore } from "@webpack/types";
@ -32,6 +33,21 @@ export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxSto
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
};
const settings = definePluginSettings({
toolTip: {
type: OptionType.BOOLEAN,
description: "If the member count should be displayed on the server tooltip",
default: true,
restartNeeded: true
},
memberList: {
type: OptionType.BOOLEAN,
description: "If the member count should be displayed on the member list",
default: true,
restartNeeded: true
}
});
const sharedIntlNumberFormat = new Intl.NumberFormat();
export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
export const cl = classNameFactory("vc-membercount-");
@ -40,6 +56,7 @@ export default definePlugin({
name: "MemberCount",
description: "Shows the amount of online & total members in the server member list and tooltip",
authors: [Devs.Ven, Devs.Commandtechno],
settings,
patches: [
{
@ -47,17 +64,18 @@ export default definePlugin({
replacement: {
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: ":[$1?.startsWith('members')?$self.render():null,$2"
}
},
predicate: () => settings.store.memberList
},
{
find: ".invitesDisabledTooltip",
replacement: {
match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/,
replace: ",$self.renderTooltip(arguments[0].guild)]"
}
},
predicate: () => settings.store.toolTip
}
],
render: ErrorBoundary.wrap(MemberCount, { noop: true }),
renderTooltip: ErrorBoundary.wrap(guild => <MemberCount isTooltip tooltipGuildId={guild.id} />, { noop: true })
});

View file

@ -255,7 +255,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
delete msg.embeds;
delete msg.interaction;
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)
.then(m => m && FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",
message: msg

View file

@ -18,7 +18,7 @@
import "./messageLogger.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
@ -45,7 +45,7 @@ function addDeleteStyle() {
const REMOVE_HISTORY_ID = "ml-remove-history";
const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style";
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => {
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => {
const { message } = props;
const { deleted, editHistory, id, channel_id } = message;
@ -94,13 +94,12 @@ export default definePlugin({
description: "Temporarily logs deleted and edited messages.",
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN],
start() {
addDeleteStyle();
addContextMenuPatch("message", patchMessageContextMenu);
contextMenus: {
"message": patchMessageContextMenu
},
stop() {
removeContextMenuPatch("message", patchMessageContextMenu);
start() {
addDeleteStyle();
},
renderEdit(edit: { timestamp: any, content: string; }) {
@ -138,6 +137,16 @@ export default definePlugin({
],
onChange: () => addDeleteStyle()
},
logDeletes: {
type: OptionType.BOOLEAN,
description: "Whether to log deleted messages",
default: true,
},
logEdits: {
type: OptionType.BOOLEAN,
description: "Whether to log edited messages",
default: true,
},
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by bots",
@ -198,8 +207,8 @@ export default definePlugin({
return cache;
},
shouldIgnore(message: any) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
shouldIgnore(message: any, isEdit = false) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds, logEdits, logDeletes } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
return ignoreBots && message.author?.bot ||
@ -207,6 +216,7 @@ export default definePlugin({
ignoreUsers.includes(message.author?.id) ||
ignoreChannels.includes(message.channel_id) ||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
(isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
},
@ -215,7 +225,7 @@ export default definePlugin({
{
// MessageStore
// Module 171447
find: "displayName=\"MessageStore\"",
find: '"MessageStore"',
replacement: [
{
// Add deleted=true to all target messages in the MESSAGE_DELETE event
@ -242,7 +252,7 @@ export default definePlugin({
match: /(MESSAGE_UPDATE:function\((\i)\).+?)\.update\((\i)/,
replace: "$1" +
".update($3,m =>" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" +
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" +
@ -370,7 +380,7 @@ export default definePlugin({
{
// ReferencedMessageStore
// Module 778667
find: "displayName=\"ReferencedMessageStore\"",
find: '"ReferencedMessageStore"',
replacement: [
{
match: /MESSAGE_DELETE:function\((\i)\).+?},/,

View file

@ -47,8 +47,8 @@ export default definePlugin({
{
find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded
replacement: {
match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/,
replace: '($1||arguments[0].isCurrentUser)?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),'
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),'
}
},
{

View file

@ -38,8 +38,8 @@ export default definePlugin({
]
},
...[
'displayName="MessageStore"',
'displayName="ReadStateStore"'
'="MessageStore",',
'"displayName","ReadStateStore")'
].map(find => ({
find,
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,

View file

@ -27,7 +27,7 @@ export default definePlugin({
{
find: '.ensureModule("discord_rpc")',
replacement: {
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)\)}/,
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/,
replace: '.ensureModule("discord_rpc")}',
},
},

View file

@ -0,0 +1,54 @@
/*
* 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 settings = definePluginSettings({
defaultLayout: {
type: OptionType.SELECT,
options: [
{ label: "List", value: 1, default: true },
{ label: "Gallery", value: 2 }
],
description: "Which layout to use as default"
},
defaultSortOrder: {
type: OptionType.SELECT,
options: [
{ label: "Recently Active", value: 0, default: true },
{ label: "Date Posted", value: 1 }
],
description: "Which sort order to use as default"
}
});
export default definePlugin({
name: "OverrideForumDefaults",
description: "Allows you to override default forum layout/sort order. you can still change it on a per-channel basis",
authors: [Devs.Inbestigator],
patches: [
{
find: "getDefaultLayout(){",
replacement: [
{
match: /getDefaultLayout\(\){/,
replace: "$&return $self.getLayout();"
},
{
match: /getDefaultSortOrder\(\){/,
replace: "$&return $self.getSortOrder();"
}
]
}
],
getLayout: () => settings.store.defaultLayout,
getSortOrder: () => settings.store.defaultSortOrder,
settings
});

View file

@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { settings } from "..";
@ -78,6 +78,8 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
const [selectedItemIndex, selectItem] = useState(0);
const selectedItem = permissions[selectedItemIndex];
const roles = GuildStore.getRoles(guild.id);
return (
<ModalRoot
{...modalProps}
@ -100,7 +102,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
<div className={cl("perms-list")}>
{permissions.map((permission, index) => {
const user = UserStore.getUser(permission.id ?? "");
const role = guild.roles[permission.id ?? ""];
const role = roles[permission.id ?? ""];
return (
<button
@ -201,7 +203,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
id="vc-pw-view-as-role"
label="View As Role"
action={() => {
const role = guild.roles[roleId];
const role = GuildStore.getRole(guild.id, roleId);
if (!role) return;
onClose();

View file

@ -18,7 +18,7 @@
import "./styles.css";
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -107,7 +107,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
}
default: {
permissions = Object.values(guild.roles).map(role => ({
permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
type: PermissionType.Role,
...role
}));
@ -125,10 +125,10 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
}
function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
return (children, props) => () => {
return (children, props) => {
if (!props) return;
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild)))
return children;
return;
const group = findGroupChildrenByChildId(childId, children);
@ -173,19 +173,10 @@ export default definePlugin({
UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBoder} />,
userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User),
channelContextMenuPatch: makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),
guildContextMenuPatch: makeContextMenuPatch("privacy", MenuItemParentType.Guild),
start() {
addContextMenuPatch("user-context", this.userContextMenuPatch);
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
},
stop() {
removeContextMenuPatch("user-context", this.userContextMenuPatch);
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
},
contextMenus: {
"user-context": makeContextMenuPatch("roles", MenuItemParentType.User),
"channel-context": makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),
"guild-context": makeContextMenuPatch("privacy", MenuItemParentType.Guild),
"guild-header-popout": makeContextMenuPatch("privacy", MenuItemParentType.Guild)
}
});

View file

@ -67,7 +67,9 @@ export function getPermissionDescription(permission: string): ReactNode {
return "";
}
export function getSortedRoles({ roles, id }: Guild, member: GuildMember) {
export function getSortedRoles({ id }: Guild, member: GuildMember) {
const roles = GuildStore.getRoles(id);
return [...member.roles, id]
.map(id => roles[id])
.sort((a, b) => b.position - a.position);
@ -85,13 +87,13 @@ export function sortUserRoles(roles: Role[]) {
}
export function sortPermissionOverwrites<T extends { id: string; type: number; }>(overwrites: T[], guildId: string) {
const guild = GuildStore.getGuild(guildId);
const roles = GuildStore.getRoles(guildId);
return overwrites.sort((a, b) => {
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;
const roleA = guild.roles[a.id];
const roleB = guild.roles[b.id];
const roleA = roles[a.id];
const roleB = roles[b.id];
return roleB.position - roleA.position;
});

View file

@ -0,0 +1,134 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Forms, Text, TextInput, Toasts, useEffect, useState } from "@webpack/common";
import { DEFAULT_COLOR, SWATCHES } from "../constants";
import { categories, Category, createCategory, getCategory, updateCategory } from "../data";
import { forceUpdate } from "../index";
interface ColorPickerProps {
color: number | null;
showEyeDropper?: boolean;
suggestedColors?: string[];
onChange(value: number | null): void;
}
interface ColorPickerWithSwatchesProps {
defaultColor: number;
colors: number[];
value: number;
disabled?: boolean;
onChange(value: number | null): void;
renderDefaultButton?: () => React.ReactNode;
renderCustomButton?: () => React.ReactNode;
}
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>("presets,", "customColor:");
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\).{0,50}"UserSettings"/);
const cl = classNameFactory("vc-pindms-modal-");
interface Props {
categoryId: string | null;
initalChannelId: string | null;
modalProps: ModalProps;
}
function useCategory(categoryId: string | null, initalChannelId: string | null) {
const [category, setCategory] = useState<Category | null>(null);
useEffect(() => {
if (categoryId)
setCategory(getCategory(categoryId)!);
else if (initalChannelId)
setCategory({
id: Toasts.genId(),
name: `Pin Category ${categories.length + 1}`,
color: DEFAULT_COLOR,
collapsed: false,
channels: [initalChannelId]
});
}, [categoryId, initalChannelId]);
return {
category,
setCategory
};
}
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) {
const { category, setCategory } = useCategory(categoryId, initalChannelId);
if (!category) return null;
const onSave = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
if (!categoryId)
await createCategory(category);
else
await updateCategory(category);
forceUpdate();
modalProps.onClose();
};
return (
<ModalRoot {...modalProps}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{categoryId ? "Edit" : "New"} Category</Text>
</ModalHeader>
{/* form is here so when you press enter while in the text input it submits */}
<form onSubmit={onSave}>
<ModalContent className={cl("content")}>
<Forms.FormSection>
<Forms.FormTitle>Name</Forms.FormTitle>
<TextInput
value={category.name}
onChange={e => setCategory({ ...category, name: e })}
/>
</Forms.FormSection>
<Forms.FormDivider />
<Forms.FormSection>
<Forms.FormTitle>Color</Forms.FormTitle>
<ColorPickerWithSwatches
key={category.name}
defaultColor={DEFAULT_COLOR}
colors={SWATCHES}
onChange={c => setCategory({ ...category, color: c! })}
value={category.color}
renderDefaultButton={() => null}
renderCustomButton={() => (
<ColorPicker
color={category.color}
onChange={c => setCategory({ ...category, color: c! })}
key={category.name}
showEyeDropper={false}
/>
)}
/>
</Forms.FormSection>
</ModalContent>
<ModalFooter>
<Button type="submit" onClick={onSave} disabled={!category.name}>{categoryId ? "Save" : "Create"}</Button>
</ModalFooter>
</form>
</ModalRoot>
);
}
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
openModalLazy(async () => {
await requireSettingsMenu();
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initalChannelId={channelId} />;
});

View file

@ -0,0 +1,96 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Menu } from "@webpack/common";
import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
import { forceUpdate, PinOrder, settings } from "../index";
import { openCategoryModal } from "./CreateCategoryModal";
function createPinMenuItem(channelId: string) {
const pinned = isPinned(channelId);
return (
<Menu.MenuItem
id="pin-dm"
label="Pin DMs"
>
{!pinned && (
<>
<Menu.MenuItem
id="vc-add-category"
label="Add Category"
color="brand"
action={() => openCategoryModal(null, channelId)}
/>
<Menu.MenuSeparator />
{
categories.map(category => (
<Menu.MenuItem
id={`pin-category-${category.name}`}
label={category.name}
action={() => addChannelToCategory(channelId, category.id).then(forceUpdate)}
/>
))
}
</>
)}
{pinned && (
<>
<Menu.MenuItem
id="unpin-dm"
label="Unpin DM"
color="danger"
action={() => removeChannelFromCategory(channelId).then(forceUpdate)}
/>
{
settings.store.pinOrder === PinOrder.Custom && canMoveChannelInDirection(channelId, -1) && (
<Menu.MenuItem
id="move-up"
label="Move Up"
action={() => moveChannel(channelId, -1).then(forceUpdate)}
/>
)
}
{
settings.store.pinOrder === PinOrder.Custom && canMoveChannelInDirection(channelId, 1) && (
<Menu.MenuItem
id="move-down"
label="Move Down"
action={() => moveChannel(channelId, 1).then(forceUpdate)}
/>
)
}
</>
)}
</Menu.MenuItem>
);
}
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("leave-channel", children);
container?.unshift(createPinMenuItem(props.channel.id));
};
const UserContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("close-dm", children);
if (container) {
const idx = container.findIndex(c => c?.props?.id === "close-dm");
container.splice(idx, 0, createPinMenuItem(props.channel.id));
}
};
export const contextMenus = {
"gdm-context": GroupDMContext,
"user-context": UserContext
};

View file

@ -0,0 +1,32 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const DEFAULT_CHUNK_SIZE = 256;
export const DEFAULT_COLOR = 10070709;
export const SWATCHES = [
1752220,
3066993,
3447003,
10181046,
15277667,
15844367,
15105570,
15158332,
9807270,
6323595,
1146986,
2067276,
2123412,
7419530,
11342935,
12745742,
11027200,
10038562,
9936031,
5533306
];

View file

@ -1,75 +0,0 @@
/*
* 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 { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { Menu } from "@webpack/common";
import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings";
function PinMenuItem(channelId: string) {
const pinned = isPinned(channelId);
const canMove = pinned && settings.store.pinOrder === PinOrder.Custom;
return (
<>
<Menu.MenuItem
id="pin-dm"
label={pinned ? "Unpin DM" : "Pin DM"}
action={() => togglePin(channelId)}
/>
{canMove && snapshotArray[0] !== channelId && (
<Menu.MenuItem
id="move-pin-up"
label="Move Pin Up"
action={() => movePin(channelId, -1)}
/>
)}
{canMove && snapshotArray[snapshotArray.length - 1] !== channelId && (
<Menu.MenuItem
id="move-pin-down"
label="Move Pin Down"
action={() => movePin(channelId, +1)}
/>
)}
</>
);
}
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => () => {
const container = findGroupChildrenByChildId("leave-channel", children);
if (container)
container.unshift(PinMenuItem(props.channel.id));
};
const UserContext: NavContextMenuPatchCallback = (children, props) => () => {
const container = findGroupChildrenByChildId("close-dm", children);
if (container) {
const idx = container.findIndex(c => c?.props?.id === "close-dm");
container.splice(idx, 0, PinMenuItem(props.channel.id));
}
};
export function addContextMenus() {
addContextMenuPatch("gdm-context", GroupDMContext);
addContextMenuPatch("user-context", UserContext);
}
export function removeContextMenus() {
removeContextMenuPatch("gdm-context", GroupDMContext);
removeContextMenuPatch("user-context", UserContext);
}

214
src/plugins/pinDms/data.ts Normal file
View file

@ -0,0 +1,214 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { UserStore } from "@webpack/common";
import { DEFAULT_COLOR } from "./constants";
import { forceUpdate } from "./index";
export interface Category {
id: string;
name: string;
color: number;
channels: string[];
collapsed?: boolean;
}
const CATEGORY_BASE_KEY = "PinDMsCategories-";
const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs";
const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories";
const OLD_CATEGORY_KEY = "BetterPinDMsCategories-";
export let categories: Category[] = [];
export async function saveCats(cats: Category[]) {
const { id } = UserStore.getCurrentUser();
await DataStore.set(CATEGORY_BASE_KEY + id, cats);
}
export async function init() {
const id = UserStore.getCurrentUser()?.id;
await initCategories(id);
await migrateData(id);
forceUpdate();
}
export async function initCategories(userId: string) {
categories = await DataStore.get<Category[]>(CATEGORY_BASE_KEY + userId) ?? [];
}
export function getCategory(id: string) {
return categories.find(c => c.id === id);
}
export async function createCategory(category: Category) {
categories.push(category);
await saveCats(categories);
}
export async function updateCategory(category: Category) {
const index = categories.findIndex(c => c.id === category.id);
if (index === -1) return;
categories[index] = category;
await saveCats(categories);
}
export async function addChannelToCategory(channelId: string, categoryId: string) {
const category = categories.find(c => c.id === categoryId);
if (!category) return;
if (category.channels.includes(channelId)) return;
category.channels.push(channelId);
await saveCats(categories);
}
export async function removeChannelFromCategory(channelId: string) {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return;
category.channels = category.channels.filter(c => c !== channelId);
await saveCats(categories);
}
export async function removeCategory(categoryId: string) {
const catagory = categories.find(c => c.id === categoryId);
if (!catagory) return;
// catagory?.channels.forEach(c => removeChannelFromCategory(c));
categories = categories.filter(c => c.id !== categoryId);
await saveCats(categories);
}
export async function collapseCategory(id: string, value = true) {
const category = categories.find(c => c.id === id);
if (!category) return;
category.collapsed = value;
await saveCats(categories);
}
// utils
export function isPinned(id: string) {
return categories.some(c => c.channels.includes(id));
}
export function categoryLen() {
return categories.length;
}
export function getAllUncollapsedChannels() {
return categories.filter(c => !c.collapsed).map(c => c.channels).flat();
}
export function getSections() {
return categories.reduce((acc, category) => {
acc.push(category.channels.length === 0 ? 1 : category.channels.length);
return acc;
}, [] as number[]);
}
// move categories
export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {
const a = array[index];
const b = array[index + direction];
return a && b;
};
export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {
const index = categories.findIndex(m => m.id === id);
return canMoveArrayInDirection(categories, index, direction);
};
export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);
export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return false;
const index = category.channels.indexOf(channelId);
return canMoveArrayInDirection(category.channels, index, direction);
};
function swapElementsInArray(array: any[], index1: number, index2: number) {
if (!array[index1] || !array[index2]) return;
[array[index1], array[index2]] = [array[index2], array[index1]];
}
// stolen from PinDMs
export async function moveCategory(id: string, direction: -1 | 1) {
const a = categories.findIndex(m => m.id === id);
const b = a + direction;
swapElementsInArray(categories, a, b);
await saveCats(categories);
}
export async function moveChannel(channelId: string, direction: -1 | 1) {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return;
const a = category.channels.indexOf(channelId);
const b = a + direction;
swapElementsInArray(category.channels, a, b);
await saveCats(categories);
}
// migrate data
const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
async function migratePinDMs() {
if (categories.some(m => m.id === "oldPins")) {
return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
}
const pindmspins = getPinDMsPins();
// we dont want duplicate pins
const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m)));
if (difference?.length) {
categories.push({
id: "oldPins",
name: "Pins",
color: DEFAULT_COLOR,
channels: difference
});
}
await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
}
async function migrateOldCategories(userId: string) {
const oldCats = await DataStore.get<Category[]>(OLD_CATEGORY_KEY + userId);
// dont want to migrate if the user has already has categories.
if (categories.length === 0 && oldCats?.length) {
categories.push(...(oldCats.filter(m => m.id !== "oldPins")));
}
await DataStore.set(CATEGORY_MIGRATED_KEY, true);
}
export async function migrateData(userId: string) {
const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY);
if (m1 && m2) return;
// want to migrate the old categories first and then slove any conflicts with the PinDMs pins
if (!m1) await migrateOldCategories(userId);
if (!m2) await migratePinDMs();
await saveCats(categories);
}

View file

@ -1,118 +1,134 @@
/*
* 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/>.
*/
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common";
import { Channel } from "discord-types/general";
import { addContextMenus, removeContextMenus } from "./contextMenus";
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings";
import { contextMenus } from "./components/contextMenu";
import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal";
import { DEFAULT_CHUNK_SIZE } from "./constants";
import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data";
interface ChannelComponentProps {
children: React.ReactNode,
channel: Channel,
selected: boolean;
}
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
export let instance: any;
export const forceUpdate = () => instance?.props?._forceUpdate?.();
export const enum PinOrder {
LastMessage,
Custom
}
export const settings = definePluginSettings({
pinOrder: {
type: OptionType.SELECT,
description: "Which order should pinned DMs be displayed in?",
options: [
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
],
onChange: () => forceUpdate()
},
dmSectioncollapsed: {
type: OptionType.BOOLEAN,
description: "Collapse DM sections",
default: false,
onChange: () => forceUpdate()
}
});
export default definePlugin({
name: "PinDMs",
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
authors: [Devs.Ven, Devs.Strencher],
authors: [Devs.Ven, Devs.Aria],
settings,
start: addContextMenus,
stop: removeContextMenus,
usePinCount(channelIds: string[]) {
const pinnedDms = usePinnedDms();
// See comment on 2nd patch for reasoning
return channelIds.length ? [pinnedDms.size] : [];
},
getChannel(channels: Record<string, Channel>, idx: number) {
return channels[getPinAt(idx)];
},
isPinned,
getSnapshot: sortedSnapshot,
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
if (!isPinned(channelId))
return (
(rowHeight + padding) * 2 // header
+ rowHeight * snapshotArray.length // pins
+ originalOffset // original pin offset minus pins
);
return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding;
},
contextMenus,
patches: [
// Patch DM list
{
find: ".privateChannelsHeaderContainer,",
replacement: [
{
// filter Discord's privateChannelIds list to remove pins, and pass
// pinCount as prop. This needs to be here so that the entire DM list receives
// updates on pin/unpin
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i),/,
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1),"
// Filter out pinned channels from the private channel list
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/,
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))"
},
{
// sections is an array of numbers, where each element is a section and
// the number is the amount of rows. Add our pinCount in second place
// - Section 1: buttons for pages like Friends & Library
// - Section 2: our pinned dms
// - Section 3: the normal dm list
match: /(?<=renderRow:this\.renderRow,)sections:\[\i,/,
// For some reason, adding our sections when no private channels are ready yet
// makes DMs infinitely load. Thus usePinCount returns either a single element
// array with the count, or an empty array. Due to spreading, only in the former
// case will an element be added to the outer array
// Thanks for the fix, Strencher!
replace: "$&...this.props.pinCount??[],"
// Insert the pinned channels to sections
match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/,
replace: "...$self.makeProps(this,{$&})"
},
// Rendering
{
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);"
},
{
// Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages"
// lookbehind is used to lookup parameter name. We could use arguments[0], but
// if children ever is wrapped in an iife, it will break
match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=(\i)=>{.+?)/,
replace: "children:$2.section===1?'Pinned DMs':$1"
match: /"renderSection",(\i)=>{/,
replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);"
},
{
// Patch channel lookup inside renderDM
// channel=channels[channelIds[row]];
match: /(?<=renderDM=\((\i),(\i)\)=>{.*?this.state,\i=\i\[\i\],\i=)((\i)\[\i\]);/,
// section 1 is us, manually get our own channel
// section === 1 ? getChannel(channels, row) : channels[channelIds[row]];
replace: "$1===1?$self.getChannel($4,$2):$3;"
match: /(?<=span",{)className:\i\.headerText,/,
replace: "...$self.makeSpanProps(),$&"
},
// Fix Row Height
{
match: /(?<="getRowHeight",.{1,100}return 1===)\i/,
replace: "($&-$self.categoryLen())"
},
{
// Fix getRowHeight's check for whether this is the DMs section
// DMS (inlined) === section
match: /(?<=getRowHeight=\(.{2,50}?)1===\i/,
// DMS (inlined) === section - 1
replace: "$&-1"
match: /"getRowHeight",\((\i),(\i)\)=>{/,
replace: "$&if($self.isChannelHidden($1,$2))return 0;"
},
// Fix ScrollTo
{
// Override scrollToChannel to properly account for pinned channels
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
}
},
{
match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/,
replace: "[...$&,...$self.getAllUncollapsedChannels()]"
},
]
},
// forceUpdate moment
// https://regex101.com/r/kDN9fO/1
{
find: ".FRIENDS},\"friends\"",
replacement: {
match: /(?<=\i=\i=>{).{1,100}premiumTabSelected.{1,800}showDMHeader:.+?,/,
replace: "let forceUpdate = Vencord.Util.useForceUpdater();$&_forceUpdate:forceUpdate,"
}
},
// Fix Alt Up/Down navigation
{
find: ".Routes.APPLICATION_STORE&&",
@ -120,16 +136,225 @@ export default definePlugin({
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
}
},
// fix alt+shift+up/down
{
find: ".getFlattenedGuildIds()],",
replacement: {
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
}
},
]
],
sections: null as number[] | null,
set _instance(i: any) {
this.instance = i;
instance = i;
},
startAt: StartAt.WebpackReady,
start: init,
flux: {
CONNECTION_OPEN: init,
},
isPinned,
categoryLen,
getSections,
getAllUncollapsedChannels,
requireSettingsMenu,
makeProps(instance, { sections }: { sections: number[]; }) {
this._instance = instance;
this.sections = sections;
this.sections.splice(1, 0, ...this.getSections());
if (this.instance?.props?.privateChannelIds?.length === 0) {
// dont render direct messages header
this.sections[this.sections.length - 1] = 0;
}
return {
sections: this.sections,
chunkSize: this.getChunkSize(),
};
},
makeSpanProps() {
return {
onClick: () => this.collapseDMList(),
role: "button",
style: { cursor: "pointer" }
};
},
getChunkSize() {
// the chunk size is the amount of rows (measured in pixels) that are rendered at once (probably)
// the higher the chunk size, the more rows are rendered at once
// also if the chunk size is 0 it will render everything at once
const sections = this.getSections();
const sectionHeaderSizePx = sections.length * 40;
// (header heights + DM heights + DEFAULT_CHUNK_SIZE) * 1.5
// we multiply everything by 1.5 so it only gets unmounted after the entire list is off screen
return (sectionHeaderSizePx + sections.reduce((acc, v) => acc += v + 44, 0) + DEFAULT_CHUNK_SIZE) * 1.5;
},
isCategoryIndex(sectionIndex: number) {
return this.sections && sectionIndex > 0 && sectionIndex < this.sections.length - 1;
},
isChannelIndex(sectionIndex: number, channelIndex: number) {
if (settings.store.dmSectioncollapsed && sectionIndex !== 0)
return true;
const cat = categories[sectionIndex - 1];
return this.isCategoryIndex(sectionIndex) && (cat?.channels?.length === 0 || cat?.channels[channelIndex]);
},
isDMSectioncollapsed() {
return settings.store.dmSectioncollapsed;
},
collapseDMList() {
settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed;
forceUpdate();
},
isChannelHidden(categoryIndex: number, channelIndex: number) {
if (categoryIndex === 0) return false;
if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex)
return true;
if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;
const category = categories[categoryIndex - 1];
if (!category) return false;
return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex];
},
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
if (!isPinned(channelId))
return (
(rowHeight + padding) * 2 // header
+ rowHeight * this.getAllUncollapsedChannels().length // pins
+ originalOffset // original pin offset minus pins
);
return rowHeight * (this.getAllUncollapsedChannels().indexOf(channelId) + preRenderedChildren) + padding;
},
renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => {
const category = categories[section - 1];
if (!category) return null;
return (
<h2
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
onClick={async () => {
await collapseCategory(category.id, !category.collapsed);
forceUpdate();
}}
onContextMenu={e => {
ContextMenuApi.openContextMenu(e, () => (
<Menu.Menu
navId="vc-pindms-header-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
color="danger"
aria-label="Pin DMs Category Menu"
>
<Menu.MenuItem
id="vc-pindms-edit-category"
label="Edit Category"
action={() => openCategoryModal(category.id, null)}
/>
{
canMoveCategory(category.id) && (
<>
{
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
id="vc-pindms-move-category-up"
label="Move Up"
action={() => moveCategory(category.id, -1).then(() => forceUpdate())}
/>
}
{
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
id="vc-pindms-move-category-down"
label="Move Down"
action={() => moveCategory(category.id, 1).then(() => forceUpdate())}
/>
}
</>
)
}
<Menu.MenuSeparator />
<Menu.MenuItem
id="vc-pindms-delete-category"
color="danger"
label="Delete Category"
action={() => removeCategory(category.id).then(() => forceUpdate())}
/>
</Menu.Menu>
));
}}
>
<span className={headerClasses.headerText}>
{category?.name ?? "uh oh"}
</span>
<svg className="vc-pindms-collapse-icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
</svg>
</h2>
);
}),
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
if (!channel || !category) return null;
if (this.isChannelHidden(sectionIndex, index)) return null;
return (
<ChannelComponent
channel={channel}
selected={this.instance.props.selectedChannelId === channel.id}
>
{channel.id}
</ChannelComponent>
);
},
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
const category = categories[sectionIndex - 1];
if (!category) return { channel: null, category: null };
const channelId = this.getCategoryChannels(category)[index];
return { channel: channels[channelId], category };
},
getCategoryChannels(category: Category) {
if (category.channels.length === 0) return [];
if (settings.store.pinOrder === PinOrder.LastMessage) {
return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category.channels.includes(c));
}
return category?.channels ?? [];
}
});

View file

@ -1,94 +0,0 @@
/*
* 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 { definePluginSettings, Settings, useSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
export const enum PinOrder {
LastMessage,
Custom
}
export const settings = definePluginSettings({
pinOrder: {
type: OptionType.SELECT,
description: "Which order should pinned DMs be displayed in?",
options: [
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
]
}
});
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore");
export let snapshotArray: string[];
let snapshot: Set<string> | undefined;
const getArray = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
const save = (pins: string[]) => {
snapshot = void 0;
Settings.plugins.PinDMs.pinnedDMs = pins.join(",");
};
const takeSnapshot = () => {
snapshotArray = getArray() ?? [];
return snapshot = new Set<string>(snapshotArray);
};
const requireSnapshot = () => snapshot ?? takeSnapshot();
export function usePinnedDms() {
useSettings(["plugins.PinDMs.pinnedDMs"]);
return requireSnapshot();
}
export function isPinned(id: string) {
return requireSnapshot().has(id);
}
export function togglePin(id: string) {
const snapshot = requireSnapshot();
if (!snapshot.delete(id)) {
snapshot.add(id);
}
save([...snapshot]);
}
export function sortedSnapshot() {
requireSnapshot();
if (settings.store.pinOrder === PinOrder.LastMessage)
return PrivateChannelSortStore.getPrivateChannelIds().filter(isPinned);
return snapshotArray;
}
export function getPinAt(idx: number) {
return sortedSnapshot()[idx];
}
export function movePin(id: string, direction: -1 | 1) {
const pins = getArray()!;
const a = pins.indexOf(id);
const b = a + direction;
[pins[a], pins[b]] = [pins[b], pins[a]];
save(pins);
}

View file

@ -0,0 +1,37 @@
.vc-pindms-section-container {
box-sizing: border-box;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-transform: uppercase;
font-size: 12px;
line-height: 16px;
letter-spacing: .02em;
font-family: var(--font-display);
font-weight: 600;
flex: 1 1 auto;
color: var(--channels-default);
cursor: pointer;
}
.vc-pindms-modal-content {
display: grid;
justify-content: center;
padding: 1rem;
gap: 1.5rem;
}
.vc-pindms-modal-content [class^="defaultContainer"] {
display: none;
}
.vc-pindms-collapse-icon {
width: 16px;
height: 16px;
color: var(--interactive-normal);
transform: rotate(90deg)
}
.vc-pindms-collapsed .vc-pindms-collapse-icon {
transform: rotate(0deg);
}

View file

@ -17,8 +17,8 @@
*/
import { Settings } from "@api/Settings";
import { VENCORD_USER_AGENT } from "@utils/constants";
import { debounce } from "@utils/debounce";
import { debounce } from "@shared/debounce";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { getCurrentChannel } from "@utils/discord";
import { useAwaiter } from "@utils/react";
import { UserProfileStore, UserStore } from "@webpack/common";

View file

@ -54,7 +54,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "QuickReply",
authors: [Devs.obscurity, Devs.Ven, Devs.pylix],
authors: [Devs.fawn, Devs.Ven, Devs.pylix],
description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds",
settings,

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -49,9 +51,11 @@ const ReadAllButton = () => (
<Button
onClick={onClick}
size={Button.Sizes.MIN}
color={Button.Colors.BRAND}
style={{ marginTop: "2px", marginBottom: "8px", marginLeft: "9px" }}
>Read all</Button>
color={Button.Colors.CUSTOM}
className="vc-ranb-button"
>
Read All
</Button>
);
export default definePlugin({

View file

@ -0,0 +1,11 @@
.vc-ranb-button {
color: var(--interactive-normal);
padding: 0 0.5em;
margin-bottom: 0.5em;
width: 100%;
box-sizing: border-box;
}
.vc-ranb-button:hover {
color: var(--interactive-active);
}

View file

@ -0,0 +1,5 @@
# ResurrectHome
Brings back the phased out [Server Home](https://support.discord.com/hc/en-us/articles/6156116949911-Server-Home-Beta) feature!
![](https://github.com/Vendicated/Vencord/assets/61953774/98d5d667-bbb9-48b8-872d-c9b3980f6506)

View file

@ -0,0 +1,119 @@
/*
* 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 { findGroupChildrenByChildId } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Menu } from "@webpack/common";
const settings = definePluginSettings({
forceServerHome: {
type: OptionType.BOOLEAN,
description: "Force the Server Guide to be the Server Home tab when it is enabled.",
default: false
}
});
function useForceServerHome() {
const { forceServerHome } = settings.use(["forceServerHome"]);
return forceServerHome;
}
export default definePlugin({
name: "ResurrectHome",
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.",
authors: [Devs.Dolfies, Devs.Nuckyz],
settings,
patches: [
// Force home deprecation override
{
find: "GuildFeatures.GUILD_HOME_DEPRECATION_OVERRIDE",
all: true,
replacement: [
{
match: /\i\.hasFeature\(\i\.GuildFeatures\.GUILD_HOME_DEPRECATION_OVERRIDE\)/g,
replace: "true"
}
],
},
// Disable feedback prompts
{
find: "GuildHomeFeedbackExperiment.definition.id",
replacement: [
{
match: /return{showFeedback:.+?,setOnDismissedFeedback:(\i)}/,
replace: "return{showFeedback:false,setOnDismissedFeedback:$1}"
}
]
},
// This feature was never finished, so the patch is disabled
// Enable guild feed render mode selector
// {
// find: "2022-01_home_feed_toggle",
// replacement: [
// {
// match: /showSelector:!1/,
// replace: "showSelector:true"
// }
// ]
// },
// Fix focusMessage clearing previously cached messages and causing a loop when fetching messages around home messages
{
find: '"MessageActionCreators"',
replacement: {
match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/,
replace: "before:$1,"
}
},
// Force Server Home instead of Server Guide
{
find: "61eef9_2",
replacement: {
match: /(?<=getMutableGuildChannelsForGuild\(\i\)\);)(?=if\(null==\i\|\|)/,
replace: "if($self.useForceServerHome())return false;"
}
}
],
useForceServerHome,
contextMenus: {
"guild-context"(children, props) {
const forceServerHome = useForceServerHome();
if (!props?.guild) return;
const group = findGroupChildrenByChildId("hide-muted-channels", children);
group?.unshift(
<Menu.MenuCheckboxItem
key="force-server-home"
id="force-server-home"
label="Force Server Home"
checked={forceServerHome}
action={() => settings.store.forceServerHome = !forceServerHome}
/>
);
}
}
});

View file

@ -30,9 +30,9 @@ export default definePlugin({
patches: [
{
find: ".removeObscurity=",
find: ".removeObscurity,",
replacement: {
match: /(?<=\.removeObscurity=(\i)=>{)/,
match: /(?<="removeObscurity",(\i)=>{)/,
replace: (_, event) => `$self.reveal(${event});`
}
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Flex } from "@components/Flex";
import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
@ -84,7 +84,7 @@ function makeSearchItem(src: string) {
);
}
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (props?.reverseImageSearchType !== "img") return;
const src = props.itemHref ?? props.itemSrc;
@ -93,7 +93,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
group?.push(makeSearchItem(src));
};
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (!props?.src) return;
const group = findGroupChildrenByChildId("copy-native-link", children) ?? children;
@ -115,14 +115,8 @@ export default definePlugin({
}
}
],
start() {
addContextMenuPatch("message", messageContextMenuPatch);
addContextMenuPatch("image-context", imageContextMenuPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
removeContextMenuPatch("image-context", imageContextMenuPatch);
contextMenus: {
"message": messageContextMenuPatch,
"image-context": imageContextMenuPatch
}
});

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
import { find, findByPropsLazy } from "@webpack";
import { useAwaiter, useForceUpdater } from "@utils/react";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
import { Auth, authorize } from "../auth";
@ -31,7 +31,7 @@ import ReviewComponent from "./ReviewComponent";
const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms");
const { ChatInputTypes } = findByPropsLazy("ChatInputTypes");
const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default);
const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA", "input");
const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer");
interface UserProps {

View file

@ -18,7 +18,7 @@
import "./style.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons";
@ -36,7 +36,7 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings";
import { showToast } from "./utils";
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => {
children.push(
<Menu.MenuItem
label="View Reviews"
@ -53,6 +53,9 @@ export default definePlugin({
authors: [Devs.mantikafasi, Devs.Ven],
settings,
contextMenus: {
"guild-header-popout": guildPopoutPatch
},
patches: [
{
@ -69,8 +72,6 @@ export default definePlugin({
},
async start() {
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
const s = settings.store;
const { lastReviewId, notifyReviews } = s;
@ -127,10 +128,6 @@ export default definePlugin({
}, 4000);
},
stop() {
removeContextMenuPatch("guild-header-popout", guildPopoutPatch);
},
getReviewsComponent: ErrorBoundary.wrap((user: User) => {
const [reviewCount, setReviewCount] = useState<number>();

View file

@ -17,6 +17,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
@ -112,9 +113,8 @@ export default definePlugin({
return colorString && parseInt(colorString.slice(1), 16);
},
roleGroupColor({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) {
const guild = GuildStore.getGuild(guildId);
const role = guild?.roles[id];
roleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => {
const role = GuildStore.getRole(guildId, id);
return (
<span style={{
@ -125,7 +125,7 @@ export default definePlugin({
{title ?? label} &mdash; {count}
</span>
);
},
}, { noop: true }),
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {
return {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { ReplyIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -27,7 +27,7 @@ import { Message } from "discord-types/general";
const messageUtils = findByPropsLazy("replyToMessage");
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {
// make sure the message is in the selected channel
if (SelectedChannelStore.getChannelId() !== message.channel_id) return;
const channel = ChannelStore.getChannel(message?.channel_id);
@ -38,7 +38,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
const dmGroup = findGroupChildrenByChildId("pin", children);
if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) {
const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin");
return dmGroup.splice(pinIndex + 1, 0, (
dmGroup.splice(pinIndex + 1, 0, (
<Menu.MenuItem
id="reply"
label={i18n.Messages.MESSAGE_ACTION_REPLY}
@ -46,12 +46,13 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
/>
));
return;
}
// servers
const serverGroup = findGroupChildrenByChildId("mark-unread", children);
if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) {
return serverGroup.unshift((
serverGroup.unshift((
<Menu.MenuItem
id="reply"
label={i18n.Messages.MESSAGE_ACTION_REPLY}
@ -59,6 +60,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
/>
));
return;
}
};
@ -67,12 +69,7 @@ export default definePlugin({
name: "SearchReply",
description: "Adds a reply button to search results",
authors: [Devs.Aria],
start() {
addContextMenuPatch("message", messageContextMenuPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
contextMenus: {
"message": messageContextMenuPatch
}
});

View file

@ -16,7 +16,7 @@ export default definePlugin({
{
find: "call_ringing_beat\"",
replacement: {
match: /500===\i\.random\(1,1e3\)/,
match: /500===\i\(\)\.random\(1,1e3\)/,
replace: "true"
}
},

View file

@ -12,7 +12,7 @@ import { classes } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findExportedComponentLazy } from "@webpack";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
import { Guild, User } from "discord-types/general";
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
@ -172,7 +172,7 @@ function ServerInfoTab({ guild }: GuildProps) {
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
"Roles": Object.keys(guild.roles).length - 1, // - @everyone
"Roles": Object.keys(GuildStore.getRoles(guild.id)).length - 1, // - @everyone
};
return (

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
@ -12,7 +12,7 @@ import { Guild } from "discord-types/general";
import { openGuildProfileModal } from "./GuildProfileModal";
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => {
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => {
const group = findGroupChildrenByChildId("privacy", children);
group?.push(
@ -29,12 +29,8 @@ export default definePlugin({
description: "Allows you to view info about a server by right clicking it in the server list",
authors: [Devs.Ven, Devs.Nuckyz],
tags: ["guild", "info"],
start() {
addContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
},
stop() {
removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
contextMenus: {
"guild-context": Patch,
"guild-header-popout": Patch
}
});

View file

@ -4,8 +4,13 @@
}
.vc-gp-banner {
width: 100%;
cursor: pointer;
aspect-ratio: auto 240 / 135;
height: 334px;
width: 100%;
object-fit: cover;
overflow: clip;
overflow-clip-margin: content-box;
}
.vc-gp-header {

View file

@ -138,7 +138,7 @@ export default definePlugin({
all: true,
// Render null instead of the buttons if the channel is hidden
replacement: {
match: /(?<=renderOpenChatButton=\(\)=>{)/,
match: /(?<="renderOpenChatButton",\(\)=>{)/,
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
}
},
@ -191,10 +191,10 @@ export default definePlugin({
},
{
// Hide the new version of unreads box for hidden channels
find: '.displayName="ChannelListUnreadsStore"',
find: '="ChannelListUnreadsStore",',
replacement: {
match: /(?<=if\(null==(\i))(?=.{0,160}?getHasImportantUnread\)\(\i\))/g, // Global because Discord has multiple methods like that in the same module
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
match: /(?=&&\(0,\i\.getHasImportantUnread\)\((\i)\))/g, // Global because Discord has multiple methods like that in the same module
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
}
},
{
@ -218,19 +218,19 @@ export default definePlugin({
find: "Missing channel in Channel.renderHeaderToolbar",
replacement: [
{
match: /(?<=renderHeaderToolbar=\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
},
{
match: /(?<=renderHeaderToolbar=\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
},
{
match: /renderMobileToolbar=\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/,
match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/,
replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;`
},
{
match: /(?<=renderHeaderBar=\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/,
match: /(?<="renderHeaderBar",\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/,
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
},
{
@ -305,27 +305,27 @@ export default definePlugin({
]
},
{
find: ".avatars),children",
find: '+1]})},"overflow"))',
replacement: [
{
// Create a variable for the channel prop
match: /maxUsers:\i,users:\i.+?=(\i).+?;/,
match: /maxUsers:\i,users:\i.+?}=(\i).*?;/,
replace: (m, props) => `${m}let{shcChannel}=${props};`
},
{
// 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(shcChannel,true)?true:${m})`
replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel: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(shcChannel,true)&&${amount}<=0?0:1)`
replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel: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(shcChannel,true)&&${amount}<=0?"":${m}`
replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${m}`
}
]
},
@ -442,7 +442,7 @@ export default definePlugin({
}
},
{
find: '.displayName="GuildChannelStore"',
find: '="GuildChannelStore",',
replacement: [
{
// Make GuildChannelStore contain hidden channels
@ -465,7 +465,7 @@ export default definePlugin({
}
},
{
find: '.displayName="NowPlayingViewStore"',
find: '="NowPlayingViewStore",',
replacement: {
// Make active now voice states on hidden channels
match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/,

View file

@ -21,7 +21,7 @@ import "./spotifyStyles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { debounce } from "@utils/debounce";
import { debounce } from "@shared/debounce";
import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc";
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";

View file

@ -51,7 +51,7 @@ export default definePlugin({
},
},
{
find: '.displayName="SpotifyStore"',
find: '"displayName","SpotifyStore")',
replacement: [
{
predicate: () => settings.store.noSpotifyAutoPause,

View file

@ -22,7 +22,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "TimeBarAllActivities",
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
authors: [Devs.obscurity],
authors: [Devs.fawn],
patches: [
{
find: "}renderTimeBar(",

View file

@ -19,7 +19,7 @@
import "./styles.css";
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addButton, removeButton } from "@api/MessagePopover";
@ -32,7 +32,7 @@ import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
import { translate } from "./utils";
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => () => {
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => {
if (!message.content) return;
const group = findGroupChildrenByChildId("copy-text", children);
@ -57,13 +57,15 @@ export default definePlugin({
authors: [Devs.Ven],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
settings,
contextMenus: {
"message": messageCtxPatch
},
// not used, just here in case some other plugin wants it or w/e
translate,
start() {
addAccessory("vc-translation", props => <TranslationAccessory message={props.message} />);
addContextMenuPatch("message", messageCtxPatch);
addChatBarButton("vc-translate", TranslateChatBarIcon);
addButton("vc-translate", message => {
@ -91,7 +93,6 @@ export default definePlugin({
stop() {
removePreSendListener(this.preSend);
removeContextMenuPatch("message", messageCtxPatch);
removeChatBarButton("vc-translate");
removeButton("vc-translate");
removeAccessory("vc-translation");

View file

@ -125,7 +125,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "TypingIndicator",
description: "Adds an indicator if someone is typing on a channel.",
authors: [Devs.Nuckyz, Devs.obscurity],
authors: [Devs.Nuckyz, Devs.fawn],
settings,
patches: [

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -24,7 +24,7 @@ import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@web
const EMBED_SUPPRESSED = 1 << 2;
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => {
const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0;
if (!isEmbedSuppressed && !embeds.length) return;
@ -56,12 +56,7 @@ export default definePlugin({
name: "UnsuppressEmbeds",
authors: [Devs.rad, Devs.HypedDomi],
description: "Allows you to unsuppress embeds in messages",
start() {
addContextMenuPatch("message", messageContextMenuPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
},
contextMenus: {
"message": messageContextMenuPatch
}
});

View file

@ -98,8 +98,8 @@ export default definePlugin({
{
find: ".popularApplicationCommandIds,",
replacement: {
match: /\(0,\i\.jsx\)\(\i\.\i,{user:\i,setNote/,
replace: "$self.patchPopout(arguments[0]),$&",
match: /applicationId:\i\.id}\),(?=.{0,50}setNote:\i)/,
replace: "$&$self.patchPopout(arguments[0]),",
}
},
// below username

View file

@ -19,7 +19,7 @@
import "./index.css";
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings } from "@api/Settings";
import { Settings, useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -30,6 +30,8 @@ import type { ReactNode } from "react";
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
function VencordPopout(onClose: () => void) {
const { useQuickCss } = useSettings(["useQuickCss"]);
const pluginEntries = [] as ReactNode[];
for (const plugin of Object.values(Vencord.Plugins.plugins)) {
@ -68,11 +70,10 @@ function VencordPopout(onClose: () => void) {
/>
<Menu.MenuCheckboxItem
id="vc-toolbox-quickcss-toggle"
checked={Settings.useQuickCss}
checked={useQuickCss}
label={"Enable QuickCSS"}
action={() => {
Settings.useQuickCss = !Settings.useQuickCss;
onClose();
Settings.useQuickCss = !useQuickCss;
}}
/>
<Menu.MenuItem

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
@ -80,7 +80,7 @@ function openImage(url: string) {
});
}
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => {
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => {
if (!user) return;
const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null;
@ -109,7 +109,7 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
));
};
const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => {
const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => {
if (!guild) return;
const { id, icon, banner } = guild;
@ -155,14 +155,9 @@ export default definePlugin({
openImage,
start() {
addContextMenuPatch("user-context", UserContext);
addContextMenuPatch("guild-context", GuildContext);
},
stop() {
removeContextMenuPatch("user-context", UserContext);
removeContextMenuPatch("guild-context", GuildContext);
contextMenus: {
"user-context": UserContext,
"guild-context": GuildContext
},
patches: [
@ -179,7 +174,7 @@ export default definePlugin({
find: ".NITRO_BANNER,",
replacement: {
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
match: /style:\{(?=backgroundImage:(\i&&\i)\?"url\("\.concat\((\i),)/,
match: /style:\{(?=backgroundImage:(\i)\?"url\("\.concat\((\i),)/,
replace:
// onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
@ -117,8 +117,8 @@ const settings = definePluginSettings({
}
});
function MakeContextCallback(name: "Guild" | "User" | "Channel") {
const callback: NavContextMenuPatchCallback = (children, props) => () => {
function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback {
return (children, props) => {
const value = props[name.toLowerCase()];
if (!value) return;
if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings
@ -141,16 +141,19 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel") {
/>
);
};
return callback;
}
export default definePlugin({
name: "ViewRaw",
description: "Copy and view the raw content/data of any message, channel or guild",
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
dependencies: ["MessagePopoverAPI"],
settings,
contextMenus: {
"guild-context": MakeContextCallback("Guild"),
"channel-context": MakeContextCallback("Channel"),
"user-context": MakeContextCallback("User")
},
start() {
addButton("ViewRaw", msg => {
@ -187,16 +190,9 @@ export default definePlugin({
onContextMenu: handleContextMenu
};
});
addContextMenuPatch("guild-context", MakeContextCallback("Guild"));
addContextMenuPatch("channel-context", MakeContextCallback("Channel"));
addContextMenuPatch("user-context", MakeContextCallback("User"));
},
stop() {
removeButton("CopyRawMessage");
removeContextMenuPatch("guild-context", MakeContextCallback("Guild"));
removeContextMenuPatch("channel-context", MakeContextCallback("Channel"));
removeContextMenuPatch("user-context", MakeContextCallback("User"));
removeButton("ViewRaw");
}
});

Some files were not shown because too many files have changed in this diff Show more