Merge branch 'dev' into main

This commit is contained in:
zoey-on-github 2024-03-15 17:33:07 -07:00 committed by GitHub
commit c04036d06a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 992 additions and 496 deletions

View file

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

View file

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

View file

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

View file

@ -17,22 +17,20 @@
*/ */
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Menu, React } from "@webpack/common";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
type ContextMenuPatchCallbackReturn = (() => void) | void;
/** /**
* @param children The rendered context menu elements * @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
* @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 navId The navId of the context menu being patched
* @param children The rendered context menu elements * @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
* @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"); 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 id The id of the child. If an array is specified, all ids will be tried
* @param children The context menu children * @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) { for (const child of children) {
if (child == null) continue; if (child == null) continue;
if (Array.isArray(child)) {
const found = findGroupChildrenByChildId(id, child);
if (found !== null) return found;
}
if ( if (
(Array.isArray(id) && id.some(id => child.props?.id === id)) (Array.isArray(id) && id.some(id => child.props?.id === id))
|| child.props?.id === id || child.props?.id === id
) return _itemsArray ?? null; ) return children;
let nextChildren = child.props?.children; let nextChildren = child.props?.children;
if (nextChildren) { if (nextChildren) {
@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
child.props.children = nextChildren; child.props.children = nextChildren;
} }
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren); const found = findGroupChildrenByChildId(id, nextChildren);
if (found !== null) return found; if (found !== null) return found;
} }
} }
@ -126,9 +129,12 @@ interface ContextMenuProps {
onClose: (callback: (...args: Array<any>) => any) => void; 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 ??= []; props.contextMenuApiArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId); const contextMenuPatches = navPatches.get(props.navId);
@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) {
if (contextMenuPatches) { if (contextMenuPatches) {
for (const patch of contextMenuPatches) { for (const patch of contextMenuPatches) {
try { try {
const callback = patch(props.children, ...props.contextMenuApiArguments); patch(props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
} catch (err) { } catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
} }
@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) { for (const patch of globalPatches) {
try { try {
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments); patch(props.navId, props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
} catch (err) { } catch (err) {
ContextMenuLogger.error("Global patch errored,", 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/>. * 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 { localStorage } from "@utils/localStorage";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { mergeDefaults } from "@utils/misc"; import { mergeDefaults } from "@utils/misc";
@ -52,7 +53,6 @@ export interface Settings {
| "under-page" | "under-page"
| "window" | "window"
| undefined; | undefined;
macosTranslucency: boolean | undefined;
disableMinSize: boolean; disableMinSize: boolean;
winNativeTitleBar: boolean; winNativeTitleBar: boolean;
plugins: { plugins: {
@ -88,8 +88,6 @@ const DefaultSettings: Settings = {
frameless: false, frameless: false,
transparent: false, transparent: false,
winCtrlQ: false, winCtrlQ: false,
// Replaced by macosVibrancyStyle
macosTranslucency: undefined,
macosVibrancyStyle: undefined, macosVibrancyStyle: undefined,
disableMinSize: false, disableMinSize: false,
winNativeTitleBar: false, winNativeTitleBar: false,
@ -110,13 +108,8 @@ const DefaultSettings: Settings = {
} }
}; };
try { const settings = VencordNative.settings.get();
var settings = JSON.parse(VencordNative.settings.get()) as Settings; mergeDefaults(settings, DefaultSettings);
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 saveSettingsOnFrequentAction = debounce(async () => { const saveSettingsOnFrequentAction = debounce(async () => {
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) { if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
@ -125,76 +118,52 @@ const saveSettingsOnFrequentAction = debounce(async () => {
} }
}, 60_000); }, 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 if (path === "plugins" && key in plugins)
function makeProxy(settings: any, root = settings, path = ""): Settings { return target[key] = {
return proxyCache[path] ??= new Proxy(settings, { enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
get(target, p: string) { };
const v = target[p];
// using "in" is important in the following cases to properly handle falsy or nullish values // Since the property is not set, check if this is a plugin's setting and if so, try to resolve
if (!(p in target)) { // the default value.
// Return empty for plugins with no settings if (path.startsWith("plugins.")) {
if (path === "plugins" && p in plugins) const plugin = path.slice("plugins.".length);
return target[p] = makeProxy({ if (plugin in plugins) {
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false const setting = plugins[plugin].options?.[key];
}, root, `plugins.${p}`); if (!setting) return v;
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve if ("default" in setting)
// the default value. // normal setting with a default value
if (path.startsWith("plugins.")) { return (target[key] = setting.default);
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;
}
// Recursively proxy Objects with the updated property path if (setting.type === OptionType.SELECT) {
if (typeof v === "object" && !Array.isArray(v) && v !== null) const def = setting.options.find(o => o.default);
return makeProxy(v, root, `${path}${path && "."}${p}`); if (def)
target[key] = def.value;
// primitive or similar, no need to proxy further return def?.value;
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);
} }
} }
// 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, * 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. * the updated settings to disk.
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings} * 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 * Settings hook for React components. Returns a smart settings
@ -223,43 +192,21 @@ export const Settings = makeProxy(settings);
export function useSettings(paths?: UseSettings<Settings>[]) { export function useSettings(paths?: UseSettings<Settings>[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {}); const [, forceUpdate] = React.useReducer(() => ({}), {});
const onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
: forceUpdate;
React.useEffect(() => { React.useEffect(() => {
subscriptions.add(onUpdate); if (paths) {
return () => void subscriptions.delete(onUpdate); 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; return SettingsStore.store;
}
// 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);
} }
export function migratePluginSettings(name: string, ...oldNames: string[]) { export function migratePluginSettings(name: string, ...oldNames: string[]) {
const { plugins } = settings; const { plugins } = SettingsStore.plain;
if (name in plugins) return; if (name in plugins) return;
for (const oldName of oldNames) { 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}`); logger.info(`Migrating settings from old name ${oldName} to ${name}`);
plugins[name] = plugins[oldName]; plugins[name] = plugins[oldName];
delete plugins[oldName]; delete plugins[oldName];
VencordNative.settings.set(JSON.stringify(settings, null, 4)); SettingsStore.markAsChanged();
break; break;
} }
} }

View file

@ -18,7 +18,7 @@
import { CheckedTextInput } from "@components/CheckedTextInput"; import { CheckedTextInput } from "@components/CheckedTextInput";
import { CodeBlock } from "@components/CodeBlock"; import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@utils/debounce"; import { debounce } from "@shared/debounce";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";

View file

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

View file

@ -50,14 +50,6 @@ function VencordSettings() {
const isMac = navigator.platform.toLowerCase().startsWith("mac"); const isMac = navigator.platform.toLowerCase().startsWith("mac");
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac; 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 | { const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>; key: KeysOfType<typeof settings, boolean>;
title: string; title: string;
@ -164,7 +156,7 @@ function VencordSettings() {
options={[ options={[
// Sorted from most opaque to most transparent // 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)", label: "Under Page (window tinting)",
@ -191,9 +183,8 @@ function VencordSettings() {
value: "header" value: "header"
}, },
{ {
label: "Sidebar (old value for transparent windows)", label: "Sidebar",
value: "sidebar", value: "sidebar"
default: settings.macosTranslucency
}, },
{ {
label: "Tooltip", label: "Tooltip",

View file

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

View file

@ -18,22 +18,21 @@
import "./updater"; import "./updater";
import "./ipcPlugins"; import "./ipcPlugins";
import "./settings";
import { debounce } from "@utils/debounce"; import { debounce } from "@shared/debounce";
import { IpcEvents } from "@utils/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
import { Queue } from "@utils/Queue";
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
import { mkdirSync, readFileSync, watch } from "fs"; import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
import { open, readdir, readFile, writeFile } from "fs/promises"; import { open, readdir, readFile } from "fs/promises";
import { join, normalize } from "path"; import { join, normalize } from "path";
import monacoHtml from "~fileContent/monacoWin.html;base64"; import monacoHtml from "~fileContent/monacoWin.html;base64";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes"; 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"; import { makeLinksOpenExternally } from "./utils/externalLinks";
mkdirSync(SETTINGS_DIR, { recursive: true });
mkdirSync(THEMES_DIR, { recursive: true }); mkdirSync(THEMES_DIR, { recursive: true });
export function ensureSafePath(basePath: string, path: string) { export function ensureSafePath(basePath: string, path: string) {
@ -71,22 +70,6 @@ function getThemeData(fileName: string) {
return readFile(safePath, "utf-8"); 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_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
@ -101,12 +84,10 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
shell.openExternal(url); shell.openExternal(url);
}); });
const cssWriteQueue = new Queue();
const settingsWriteQueue = new Queue();
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => 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); ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
@ -117,25 +98,25 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}` "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) { export function initIpc(mainWindow: BrowserWindow) {
let quickCssWatcher: FSWatcher | undefined;
open(QUICKCSS_PATH, "a+").then(fd => { open(QUICKCSS_PATH, "a+").then(fd => {
fd.close(); 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()); mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
}, 50)); }, 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.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0);
})); }));
mainWindow.once("closed", () => {
quickCssWatcher?.close();
themesWatcher.close();
});
} }
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { 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/>. * 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 { ipcMain } from "electron";
import PluginNatives from "~pluginNatives"; import PluginNatives from "~pluginNatives";

View file

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { onceDefined } from "@utils/onceDefined"; import { onceDefined } from "@shared/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron"; import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import { dirname, join } from "path"; import { dirname, join } from "path";
import { getSettings, initIpc } from "./ipcMain"; import { initIpc } from "./ipcMain";
import { RendererSettings } from "./settings";
import { IS_VANILLA } from "./utils/constants"; import { IS_VANILLA } from "./utils/constants";
console.log("[Vencord] Starting up..."); console.log("[Vencord] Starting up...");
@ -41,8 +42,7 @@ require.main!.filename = join(asarPath, discordPkg.main);
app.setAppPath(asarPath); app.setAppPath(asarPath);
if (!IS_VANILLA) { if (!IS_VANILLA) {
const settings = getSettings(); const settings = RendererSettings.store;
// Repatch after host updates on Windows // Repatch after host updates on Windows
if (process.platform === "win32") { if (process.platform === "win32") {
require("./patchWin32Updater"); require("./patchWin32Updater");
@ -84,13 +84,11 @@ if (!IS_VANILLA) {
options.backgroundColor = "#00000000"; options.backgroundColor = "#00000000";
} }
const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency); const needsVibrancy = process.platform === "darwin" && settings.macosVibrancyStyle;
if (needsVibrancy) { if (needsVibrancy) {
options.backgroundColor = "#00000000"; options.backgroundColor = "#00000000";
if (settings.macosTranslucency) { if (settings.macosVibrancyStyle) {
options.vibrancy = "sidebar";
} else if (settings.macosVibrancyStyle) {
options.vibrancy = 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/>. * 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 { execFile as cpExecFile } from "child_process";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { join } from "path"; import { join } from "path";
@ -49,9 +49,12 @@ async function getRepo() {
async function calculateGitChanges() { async function calculateGitChanges() {
await git("fetch"); 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(); const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => { 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { VENCORD_USER_AGENT } from "@utils/constants"; import { IpcEvents } from "@shared/IpcEvents";
import { IpcEvents } from "@utils/IpcEvents"; import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { writeFile } from "fs/promises"; import { writeFile } from "fs/promises";
import { join } from "path"; import { join } from "path";
@ -53,7 +53,7 @@ async function calculateGitChanges() {
// github api only sends the long sha // github api only sends the long sha
hash: c.sha.slice(0, 7), hash: c.sha.slice(0, 7),
author: c.author.login, 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 THEMES_DIR = join(DATA_DIR, "themes");
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
export const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, "native-settings.json");
export const ALLOWED_PROTOCOLS = [ export const ALLOWED_PROTOCOLS = [
"https:", "https:",
"http:", "http:",

View file

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

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addContextMenuPatch } from "@api/ContextMenu"; import { findGroupChildrenByChildId } from "@api/ContextMenu";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -30,21 +30,21 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Megu], authors: [Devs.Ven, Devs.Megu],
required: true, required: true,
start() { contextMenus: {
// The settings shortcuts in the user settings cog context menu // The settings shortcuts in the user settings cog context menu
// read the elements from a hardcoded map which for obvious reason // read the elements from a hardcoded map which for obvious reason
// doesn't contain our sections. This patches the actions of our // doesn't contain our sections. This patches the actions of our
// sections to manually use SettingsRouter (which only works on desktop // sections to manually use SettingsRouter (which only works on desktop
// but the context menu is usually not available on mobile anyway) // but the context menu is usually not available on mobile anyway)
addContextMenuPatch("user-settings-cog", children => () => { "user-settings-cog"(children) {
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any; const section = findGroupChildrenByChildId("VencordSettings", children);
section?.forEach(c => { section?.forEach(c => {
const id = c?.props?.id; const id = c?.props?.id;
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) { if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
c.props.action = () => SettingsRouter.open(id); c!.props.action = () => SettingsRouter.open(id);
} }
}); });
}); }
}, },
patches: [{ patches: [{

View file

@ -67,7 +67,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "AnonymiseFileNames", name: "AnonymiseFileNames",
authors: [Devs.obscurity], authors: [Devs.fawn],
description: "Anonymise uploaded file names", description: "Anonymise uploaded file names",
patches: [ patches: [
{ {

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

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

View file

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

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; 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)"); 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]; return (result === null) ? null : result[1];
} }
function mapReject(arr, mapFunc, rejectFunc = _.isNull) { function mapReject(arr, mapFunc) {
return _.reject(arr.map(mapFunc), rejectFunc); return arr.map(mapFunc).filter(Boolean);
} }
function updateColorVars(color: string) { function updateColorVars(color: string) {

View file

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

View file

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

View file

@ -131,9 +131,10 @@ export default definePlugin({
getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) { getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {
// Only Decor avatar decorations have this SKU ID // Only Decor avatar decorations have this SKU ID
if (avatarDecoration?.skuId === SKU_ID) { if (avatarDecoration?.skuId === SKU_ID) {
const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`); const parts = avatarDecoration.asset.split("_");
url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString()); // Remove a_ prefix if it's animated and animation is disabled
return url.toString(); if (isAnimatedAvatarDecoration(avatarDecoration.asset) && !canAnimate) parts.shift();
return `${CDN_URL}/${parts.join("_")}.png`;
} else if (avatarDecoration?.skuId === RAW_SKU_ID) { } else if (avatarDecoration?.skuId === RAW_SKU_ID) {
return avatarDecoration.asset; return avatarDecoration.asset;
} }

View file

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

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { CheckedTextInput } from "@components/CheckedTextInput"; import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
@ -312,7 +312,7 @@ function isGifUrl(url: string) {
return new URL(url).pathname.endsWith(".gif"); return new URL(url).pathname.endsWith(".gif");
} }
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {}; const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
if (!favoriteableId) return; if (!favoriteableId) return;
@ -341,7 +341,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
findGroupChildrenByChildId("copy-link", children)?.push(menuItem); 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 ?? {}; const { id, name, type } = props?.target?.dataset ?? {};
if (!id) return; if (!id) return;
@ -363,14 +363,8 @@ export default definePlugin({
description: "Allows you to clone Emotes & Stickers to your own server (right click them)", description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
tags: ["StickerCloner"], tags: ["StickerCloner"],
authors: [Devs.Ven, Devs.Nuckyz], authors: [Devs.Ven, Devs.Nuckyz],
contextMenus: {
start() { "message": messageContextMenuPatch,
addContextMenuPatch("message", messageContextMenuPatch); "expression-picker": expressionPickerPatch
addContextMenuPatch("expression-picker", expressionPickerPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
removeContextMenuPatch("expression-picker", expressionPickerPatch);
} }
}); });

View file

@ -162,7 +162,7 @@ const settings = definePluginSettings({
default: true default: true
}, },
hyperLinkText: { 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, type: OptionType.STRING,
default: "{{NAME}}" default: "{{NAME}}"
} }
@ -185,7 +185,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
export default definePlugin({ export default definePlugin({
name: "FakeNitro", 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.", description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
dependencies: ["MessageEventsAPI"], dependencies: ["MessageEventsAPI"],
@ -585,13 +585,15 @@ export default definePlugin({
for (const [index, child] of children.entries()) children[index] = modifyChild(child); for (const [index, child] of children.entries()) children[index] = modifyChild(child);
children = this.clearEmptyArrayItems(children); children = this.clearEmptyArrayItems(children);
this.trimContent(children);
return children; return children;
}; };
try { try {
return modifyChildren(lodash.cloneDeep(content)); const newContent = modifyChildren(lodash.cloneDeep(content));
this.trimContent(newContent);
return newContent;
} catch (err) { } catch (err) {
new Logger("FakeNitro").error(err); new Logger("FakeNitro").error(err);
return content; return content;
@ -791,8 +793,8 @@ export default definePlugin({
title: "Hold on!", title: "Hold on!",
body: <div> body: <div>
<Forms.FormText> <Forms.FormText>
You are trying to send/edit a message that contains a FakeNitro emoji or sticker 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. 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. Are you sure you want to send this message? Your FakeNitro items will appear as a link only.
</Forms.FormText> </Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}> <Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
@ -864,7 +866,9 @@ export default definePlugin({
const url = new URL(link); const url = new URL(link);
url.searchParams.set("name", sticker.name); 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; extra.stickers!.length = 0;
} }
} }

View file

@ -200,7 +200,14 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
export function getTargetString(urlStr: string) { 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) { switch (settings.store.searchOption) {
case "url": case "url":
return url.href; return url.href;

View file

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

View file

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

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

View file

@ -17,6 +17,7 @@
*/ */
import { registerCommand, unregisterCommand } from "@api/Commands"; import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Patch, Plugin, StartAt } from "@utils/types"; 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) { export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
const { name, commands, flux } = p; const { name, commands, flux, contextMenus } = p;
if (p.start) { if (p.start) {
logger.info("Starting plugin", name); 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; return true;
}, p => `startPlugin ${p.name}`); }, p => `startPlugin ${p.name}`);
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) { export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
const { name, commands, flux } = p; const { name, commands, flux, contextMenus } = p;
if (p.stop) { if (p.stop) {
logger.info("Stopping plugin", name); logger.info("Stopping plugin", name);
if (!p.started) { 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; return true;
}, p => `stopPlugin ${p.name}`); }, p => `stopPlugin ${p.name}`);

View file

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

View file

@ -18,7 +18,7 @@
import "./messageLogger.css"; import "./messageLogger.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles"; import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -45,7 +45,7 @@ function addDeleteStyle() {
const REMOVE_HISTORY_ID = "ml-remove-history"; const REMOVE_HISTORY_ID = "ml-remove-history";
const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style"; const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style";
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => { const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => {
const { message } = props; const { message } = props;
const { deleted, editHistory, id, channel_id } = message; const { deleted, editHistory, id, channel_id } = message;
@ -94,13 +94,12 @@ export default definePlugin({
description: "Temporarily logs deleted and edited messages.", description: "Temporarily logs deleted and edited messages.",
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN], authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN],
start() { contextMenus: {
addDeleteStyle(); "message": patchMessageContextMenu
addContextMenuPatch("message", patchMessageContextMenu);
}, },
stop() { start() {
removeContextMenuPatch("message", patchMessageContextMenu); addDeleteStyle();
}, },
renderEdit(edit: { timestamp: any, content: string; }) { renderEdit(edit: { timestamp: any, content: string; }) {

View file

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

View file

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

View file

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

View file

@ -67,7 +67,9 @@ export function getPermissionDescription(permission: string): ReactNode {
return ""; 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] return [...member.roles, id]
.map(id => roles[id]) .map(id => roles[id])
.sort((a, b) => b.position - a.position); .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) { 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) => { return overwrites.sort((a, b) => {
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0; if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;
const roleA = guild.roles[a.id]; const roleA = roles[a.id];
const roleB = guild.roles[b.id]; const roleB = roles[b.id];
return roleB.position - roleA.position; return roleB.position - roleA.position;
}); });

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Menu } from "@webpack/common"; import { Menu } from "@webpack/common";
import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings"; import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings";
@ -50,13 +50,13 @@ function PinMenuItem(channelId: string) {
); );
} }
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => () => { const GroupDMContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("leave-channel", children); const container = findGroupChildrenByChildId("leave-channel", children);
if (container) if (container)
container.unshift(PinMenuItem(props.channel.id)); container.unshift(PinMenuItem(props.channel.id));
}; };
const UserContext: NavContextMenuPatchCallback = (children, props) => () => { const UserContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("close-dm", children); const container = findGroupChildrenByChildId("close-dm", children);
if (container) { if (container) {
const idx = container.findIndex(c => c?.props?.id === "close-dm"); const idx = container.findIndex(c => c?.props?.id === "close-dm");
@ -64,12 +64,7 @@ const UserContext: NavContextMenuPatchCallback = (children, props) => () => {
} }
}; };
export function addContextMenus() { export const contextMenus = {
addContextMenuPatch("gdm-context", GroupDMContext); "gdm-context": GroupDMContext,
addContextMenuPatch("user-context", UserContext); "user-context": UserContext
} };
export function removeContextMenus() {
removeContextMenuPatch("gdm-context", GroupDMContext);
removeContextMenuPatch("user-context", UserContext);
}

View file

@ -20,18 +20,16 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Channel } from "discord-types/general"; import { Channel } from "discord-types/general";
import { addContextMenus, removeContextMenus } from "./contextMenus"; import { contextMenus } from "./contextMenus";
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings"; import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings";
export default definePlugin({ export default definePlugin({
name: "PinDMs", 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", 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],
settings, settings,
contextMenus,
start: addContextMenus,
stop: removeContextMenus,
usePinCount(channelIds: string[]) { usePinCount(channelIds: string[]) {
const pinnedDms = usePinnedDms(); const pinnedDms = usePinnedDms();

View file

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

View file

@ -54,7 +54,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "QuickReply", 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", description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds",
settings, settings,

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:\i,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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ import { classes } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findExportedComponentLazy } from "@webpack"; 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"; import { Guild, User } from "discord-types/general";
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper"); const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
@ -172,7 +172,7 @@ function ServerInfoTab({ guild }: GuildProps) {
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?", "Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`, "Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category "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 ( return (

View file

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

View file

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

View file

@ -305,27 +305,27 @@ export default definePlugin({
] ]
}, },
{ {
find: ".avatars),children", find: '+1]})},"overflow"))',
replacement: [ replacement: [
{ {
// Create a variable for the channel prop // 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};` replace: (m, props) => `${m}let{shcChannel}=${props};`
}, },
{ {
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen // Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
match: /\i>0(?=&&.{0,60}renderPopout)/, 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 // 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)/, 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 // 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/, 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}`
} }
] ]
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { ImageIcon } from "@components/Icons"; import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; 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; if (!user) return;
const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null; 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; if (!guild) return;
const { id, icon, banner } = guild; const { id, icon, banner } = guild;
@ -155,14 +155,9 @@ export default definePlugin({
openImage, openImage,
start() { contextMenus: {
addContextMenuPatch("user-context", UserContext); "user-context": UserContext,
addContextMenuPatch("guild-context", GuildContext); "guild-context": GuildContext
},
stop() {
removeContextMenuPatch("user-context", UserContext);
removeContextMenuPatch("guild-context", GuildContext);
}, },
patches: [ patches: [

View file

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

View file

@ -18,7 +18,7 @@
import "./styles.css"; import "./styles.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Microphone } from "@components/Icons"; import { Microphone } from "@components/Icons";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -48,18 +48,30 @@ export type VoiceRecorder = ComponentType<{
const VoiceRecorder = IS_DISCORD_DESKTOP ? VoiceRecorderDesktop : VoiceRecorderWeb; const VoiceRecorder = IS_DISCORD_DESKTOP ? VoiceRecorderDesktop : VoiceRecorderWeb;
const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return;
children.push(
<Menu.MenuItem
id="vc-send-vmsg"
label={
<div className={OptionClasses.optionLabel}>
<Microphone className={OptionClasses.optionIcon} height={24} width={24} />
<div className={OptionClasses.optionName}>Send voice message</div>
</div>
}
action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}
/>
);
};
export default definePlugin({ export default definePlugin({
name: "VoiceMessages", name: "VoiceMessages",
description: "Allows you to send voice messages like on mobile. To do so, right click the upload button and click Send Voice Message", description: "Allows you to send voice messages like on mobile. To do so, right click the upload button and click Send Voice Message",
authors: [Devs.Ven, Devs.Vap, Devs.Nickyux], authors: [Devs.Ven, Devs.Vap, Devs.Nickyux],
settings, settings,
contextMenus: {
start() { "channel-attach": ctxMenuPatch
addContextMenuPatch("channel-attach", ctxMenuPatch);
},
stop() {
removeContextMenuPatch("channel-attach", ctxMenuPatch);
} }
}); });
@ -234,20 +246,3 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) {
</ModalRoot> </ModalRoot>
); );
} }
const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return;
children.push(
<Menu.MenuItem
id="vc-send-vmsg"
label={
<div className={OptionClasses.optionLabel}>
<Microphone className={OptionClasses.optionIcon} height={24} width={24} />
<div className={OptionClasses.optionName}>Send voice message</div>
</div>
}
action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}
/>
);
};

View file

@ -196,7 +196,7 @@ export default definePlugin({
if (message.mention_roles.length > 0) { if (message.mention_roles.length > 0) {
for (const roleId of message.mention_roles) { for (const roleId of message.mention_roles) {
const role = GuildStore.getGuild(channel.guild_id).roles[roleId]; const role = GuildStore.getRole(channel.guild_id, roleId);
if (!role) continue; if (!role) continue;
const roleColor = role.colorString ?? `#${pingColor}`; const roleColor = role.colorString ?? `#${pingColor}`;
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`); finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { debounce } from "@utils/debounce"; import { debounce } from "@shared/debounce";
import { contextBridge, webFrame } from "electron"; import { contextBridge, webFrame } from "electron";
import { readFileSync, watch } from "fs"; import { readFileSync, watch } from "fs";
import { join } from "path"; import { join } from "path";

182
src/shared/SettingsStore.ts Normal file
View file

@ -0,0 +1,182 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { LiteralUnion } from "type-fest";
// 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 `${infer Pre}.${infer Suf}`
? Pre extends keyof T
? ResolvePropDeep<T[Pre], Suf>
: any
: P extends keyof T
? T[P]
: any;
interface SettingsStoreOptions {
readOnly?: boolean;
getDefaultValue?: (data: {
target: any;
key: string;
root: any;
path: string;
}) => any;
}
// merges the SettingsStoreOptions type into the class
export interface SettingsStore<T extends object> extends SettingsStoreOptions { }
/**
* The SettingsStore allows you to easily create a mutable store that
* has support for global and path-based change listeners.
*/
export class SettingsStore<T extends object> {
private pathListeners = new Map<string, Set<(newData: any) => void>>();
private globalListeners = new Set<(newData: T, path: string) => void>();
/**
* The store object. Making changes to this object will trigger the applicable change listeners
*/
public declare store: T;
/**
* The plain data. Changes to this object will not trigger any change listeners
*/
public declare plain: T;
public constructor(plain: T, options: SettingsStoreOptions = {}) {
this.plain = plain;
this.store = this.makeProxy(plain);
Object.assign(this, options);
}
private makeProxy(object: any, root: T = object, path: string = "") {
const self = this;
return new Proxy(object, {
get(target, key: string) {
let v = target[key];
if (!(key in target) && self.getDefaultValue) {
v = self.getDefaultValue({
target,
key,
root,
path
});
}
if (typeof v === "object" && v !== null && !Array.isArray(v))
return self.makeProxy(v, root, `${path}${path && "."}${key}`);
return v;
},
set(target, key: string, value) {
if (target[key] === value) return true;
Reflect.set(target, key, value);
const setPath = `${path}${path && "."}${key}`;
self.globalListeners.forEach(cb => cb(value, setPath));
self.pathListeners.get(setPath)?.forEach(cb => cb(value));
return true;
}
});
}
/**
* Set the data of the store.
* This will update this.store and this.plain (and old references to them will be stale! Avoid storing them in variables)
*
* Additionally, all global listeners (and those for pathToNotify, if specified) will be called with the new data
* @param value New data
* @param pathToNotify Optional path to notify instead of globally. Used to transfer path via ipc
*/
public setData(value: T, pathToNotify?: string) {
if (this.readOnly) throw new Error("SettingsStore is read-only");
this.plain = value;
this.store = this.makeProxy(value);
if (pathToNotify) {
let v = value;
const path = pathToNotify.split(".");
for (const p of path) {
if (!v) {
console.warn(
`Settings#setData: Path ${pathToNotify} does not exist in new data. Not dispatching update`
);
return;
}
v = v[p];
}
this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v));
}
this.markAsChanged();
}
/**
* Add a global change listener, that will fire whenever any setting is changed
*
* @param data The new data. This is either the new value set on the path, or the new root object if it was changed
* @param path The path of the setting that was changed. Empty string if the root object was changed
*/
public addGlobalChangeListener(cb: (data: any, path: string) => void) {
this.globalListeners.add(cb);
}
/**
* Add a scoped change listener that will fire whenever a setting matching the specified path is changed.
*
* For example if path is `"foo.bar"`, the listener will fire on
* ```js
* Setting.store.foo.bar = "hi"
* ```
* but not on
* ```js
* Setting.store.foo.baz = "hi"
* ```
* @param path
* @param cb
*/
public addChangeListener<P extends LiteralUnion<keyof T, string>>(
path: P,
cb: (data: ResolvePropDeep<T, P>) => void
) {
const listeners = this.pathListeners.get(path as string) ?? new Set();
listeners.add(cb);
this.pathListeners.set(path as string, listeners);
}
/**
* Remove a global listener
* @see {@link addGlobalChangeListener}
*/
public removeGlobalChangeListener(cb: (data: any, path: string) => void) {
this.globalListeners.delete(cb);
}
/**
* Remove a scoped listener
* @see {@link addChangeListener}
*/
public removeChangeListener(path: LiteralUnion<keyof T, string>, cb: (data: any) => void) {
const listeners = this.pathListeners.get(path as string);
if (!listeners) return;
listeners.delete(cb);
if (!listeners.size) this.pathListeners.delete(path as string);
}
/**
* Call all global change listeners
*/
public markAsChanged() {
this.globalListeners.forEach(cb => cb(this.plain, ""));
}
}

View file

@ -0,0 +1,12 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
export { gitHash, gitRemote };
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`;

View file

@ -16,17 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
export {
gitHash,
gitRemote
};
export const WEBPACK_CHUNK = "webpackChunkdiscord_app"; export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`;
export const SUPPORT_CHANNEL_ID = "1026515880080842772"; export const SUPPORT_CHANNEL_ID = "1026515880080842772";
export interface Dev { export interface Dev {
@ -66,8 +57,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "botato", name: "botato",
id: 440990343899643943n id: 440990343899643943n
}, },
obscurity: { fawn: {
name: "obscurity", name: "fawn",
id: 336678828233588736n, id: 336678828233588736n,
}, },
rushii: { rushii: {
@ -291,10 +282,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "RyanCaoDev", name: "RyanCaoDev",
id: 952235800110694471n, id: 952235800110694471n,
}, },
Strencher: {
name: "Strencher",
id: 415849376598982656n
},
FieryFlames: { FieryFlames: {
name: "Fiery", name: "Fiery",
id: 890228870559698955n id: 890228870559698955n
@ -418,6 +405,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
Av32000: { Av32000: {
name: "Av32000", name: "Av32000",
id: 593436735380127770n, id: 593436735380127770n,
},
Kyuuhachi: {
name: "Kyuuhachi",
id: 236588665420251137n,
},
Elvyra: {
name: "Elvyra",
id: 708275751816003615n,
} }
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);

View file

@ -16,9 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export * from "../shared/debounce";
export * from "../shared/onceDefined";
export * from "./ChangeList"; export * from "./ChangeList";
export * from "./constants"; export * from "./constants";
export * from "./debounce";
export * from "./discord"; export * from "./discord";
export * from "./guards"; export * from "./guards";
export * from "./lazy"; export * from "./lazy";
@ -27,7 +28,6 @@ export * from "./Logger";
export * from "./margins"; export * from "./margins";
export * from "./misc"; export * from "./misc";
export * from "./modal"; export * from "./modal";
export * from "./onceDefined";
export * from "./onlyOnce"; export * from "./onlyOnce";
export * from "./patches"; export * from "./patches";
export * from "./Queue"; export * from "./Queue";

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addSettingsListener, Settings } from "@api/Settings"; import { Settings, SettingsStore } from "@api/Settings";
let style: HTMLStyleElement; let style: HTMLStyleElement;
@ -81,10 +81,10 @@ document.addEventListener("DOMContentLoaded", () => {
initThemes(); initThemes();
toggle(Settings.useQuickCss); toggle(Settings.useQuickCss);
addSettingsListener("useQuickCss", toggle); SettingsStore.addChangeListener("useQuickCss", toggle);
addSettingsListener("themeLinks", initThemes); SettingsStore.addChangeListener("themeLinks", initThemes);
addSettingsListener("enabledThemes", initThemes); SettingsStore.addChangeListener("enabledThemes", initThemes);
if (!IS_WEB) if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes); VencordNative.quickCss.addThemeChangeListener(initThemes);

View file

@ -36,14 +36,14 @@ export async function importSettings(data: string) {
if ("settings" in parsed && "quickCss" in parsed) { if ("settings" in parsed && "quickCss" in parsed) {
Object.assign(PlainSettings, parsed.settings); Object.assign(PlainSettings, parsed.settings);
await VencordNative.settings.set(JSON.stringify(parsed.settings, null, 4)); await VencordNative.settings.set(parsed.settings);
await VencordNative.quickCss.set(parsed.quickCss); await VencordNative.quickCss.set(parsed.quickCss);
} else } else
throw new Error("Invalid Settings. Is this even a Vencord Settings file?"); throw new Error("Invalid Settings. Is this even a Vencord Settings file?");
} }
export async function exportSettings({ minify }: { minify?: boolean; } = {}) { export async function exportSettings({ minify }: { minify?: boolean; } = {}) {
const settings = JSON.parse(VencordNative.settings.get()); const settings = VencordNative.settings.get();
const quickCss = await VencordNative.quickCss.get(); const quickCss = await VencordNative.quickCss.get();
return JSON.stringify({ settings, quickCss }, null, minify ? undefined : 4); return JSON.stringify({ settings, quickCss }, null, minify ? undefined : 4);
} }
@ -137,7 +137,7 @@ export async function putCloudSettings(manual?: boolean) {
const { written } = await res.json(); const { written } = await res.json();
PlainSettings.cloud.settingsSyncVersion = written; PlainSettings.cloud.settingsSyncVersion = written;
VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4)); VencordNative.settings.set(PlainSettings);
cloudSettingsLogger.info("Settings uploaded to cloud successfully"); cloudSettingsLogger.info("Settings uploaded to cloud successfully");
@ -222,7 +222,7 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
// sync with server timestamp instead of local one // sync with server timestamp instead of local one
PlainSettings.cloud.settingsSyncVersion = written; PlainSettings.cloud.settingsSyncVersion = written;
VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4)); VencordNative.settings.set(PlainSettings);
cloudSettingsLogger.info("Settings loaded from cloud successfully"); cloudSettingsLogger.info("Settings loaded from cloud successfully");
if (shouldNotify) if (shouldNotify)

View file

@ -17,6 +17,7 @@
*/ */
import { Command } from "@api/Commands"; import { Command } from "@api/Commands";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
import { Promisable } from "type-fest"; import { Promisable } from "type-fest";
@ -115,6 +116,10 @@ export interface PluginDef {
flux?: { flux?: {
[E in FluxEvents]?: (event: any) => void; [E in FluxEvents]?: (event: any) => void;
}; };
/**
* Allows you to manipulate context menus
*/
contextMenus?: Record<string, NavContextMenuPatchCallback>;
/** /**
* Allows you to add custom actions to the Vencord Toolbox. * Allows you to add custom actions to the Vencord Toolbox.
* The key will be used as text for the button * The key will be used as text for the button

View file

@ -51,7 +51,7 @@ export let Avatar: t.Avatar;
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */ /** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
export let useToken: t.useToken; export let useToken: t.useToken;
export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", m => m?.type?.toString().includes("MASKED_LINK)")); export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", filters.componentByCode("MASKED_LINK)"));
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]); export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);

View file

@ -6,7 +6,10 @@
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact"); import * as t from "./types/settingsStores";
export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame");
export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact") as Record<string, t.SettingsStore>;
export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame") as Record<string, t.SettingsStore>;
export const UserSettingsActionCreators = findByPropsLazy("PreloadedUserSettingsActionCreators"); export const UserSettingsActionCreators = findByPropsLazy("PreloadedUserSettingsActionCreators");

View file

@ -46,7 +46,7 @@ export let ReadStateStore: GenericStore;
export let PresenceStore: GenericStore; export let PresenceStore: GenericStore;
export let PoggerModeSettingsStore: GenericStore; export let PoggerModeSettingsStore: GenericStore;
export let GuildStore: Stores.GuildStore & t.FluxStore; export let GuildStore: t.GuildStore;
export let UserStore: Stores.UserStore & t.FluxStore; export let UserStore: Stores.UserStore & t.FluxStore;
export let UserProfileStore: GenericStore; export let UserProfileStore: GenericStore;
export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore; export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore;

View file

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export * from "./classes";
export * from "./components"; export * from "./components";
export * from "./fluxEvents"; export * from "./fluxEvents";
export * from "./i18nMessages";
export * from "./menu"; export * from "./menu";
export * from "./settingsStores";
export * from "./stores"; export * from "./stores";
export * from "./utils"; export * from "./utils";

View file

@ -0,0 +1,11 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface SettingsStore<T = any> {
getSetting(): T;
updateSetting(value: T): void;
useSetting(): T;
}

View file

@ -17,7 +17,7 @@
*/ */
import { DraftType } from "@webpack/common"; import { DraftType } from "@webpack/common";
import { Channel } from "discord-types/general"; import { Channel, Guild, Role } from "discord-types/general";
import { FluxDispatcher, FluxEvents } from "./utils"; import { FluxDispatcher, FluxEvents } from "./utils";
@ -172,3 +172,13 @@ export class DraftStore extends FluxStore {
getThreadDraftWithParentMessageId?(arg: any): any; getThreadDraftWithParentMessageId?(arg: any): any;
getThreadSettings(channelId: string): any | null; getThreadSettings(channelId: string): any | null;
} }
export class GuildStore extends FluxStore {
getGuild(guildId: string): Guild;
getGuildCount(): number;
getGuilds(): Record<string, Guild>;
getGuildIds(): string[];
getRole(guildId: string, roleId: string): Role;
getRoles(guildId: string): Record<string, Role>;
getAllGuildRoles(): Record<string, Record<string, Role>>;
}

View file

@ -60,6 +60,7 @@ export const filters = {
return m => { return m => {
if (filter(m)) return true; if (filter(m)) return true;
if (!m.$$typeof) return false; if (!m.$$typeof) return false;
if (m.type && m.type.render) return filter(m.type.render); // memo + forwardRef
if (m.type) return filter(m.type); // memos if (m.type) return filter(m.type); // memos
if (m.render) return filter(m.render); // forwardRefs if (m.render) return filter(m.render); // forwardRefs
return false; return false;
@ -475,8 +476,10 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback
else if (typeof filter !== "function") else if (typeof filter !== "function")
throw new Error("filter must be a string, string[] or function, got " + typeof filter); throw new Error("filter must be a string, string[] or function, got " + typeof filter);
const [existing, id] = find(filter!, { isIndirect: true, isWaitFor: true }); if (cache != null) {
if (existing) return void callback(existing, id); const [existing, id] = find(filter, { isIndirect: true, isWaitFor: true });
if (existing) return void callback(existing, id);
}
subscriptions.set(filter, callback); subscriptions.set(filter, callback);
} }

View file

@ -11,7 +11,7 @@
"esnext.asynciterable", "esnext.asynciterable",
"esnext.symbol" "esnext.symbol"
], ],
"module": "commonjs", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"strict": true, "strict": true,
"noImplicitAny": false, "noImplicitAny": false,
@ -20,13 +20,15 @@
"baseUrl": "./src/", "baseUrl": "./src/",
"paths": { "paths": {
"@main/*": ["./main/*"],
"@api/*": ["./api/*"], "@api/*": ["./api/*"],
"@components/*": ["./components/*"], "@components/*": ["./components/*"],
"@utils/*": ["./utils/*"], "@utils/*": ["./utils/*"],
"@shared/*": ["./shared/*"],
"@webpack/types": ["./webpack/common/types"], "@webpack/types": ["./webpack/common/types"],
"@webpack/common": ["./webpack/common"], "@webpack/common": ["./webpack/common"],
"@webpack": ["./webpack/webpack"] "@webpack": ["./webpack/webpack"]
} }
}, },
"include": ["src/**/*"] "include": ["src/**/*", "browser/**/*", "scripts/**/*"]
} }