1
0
Fork 1
mirror of https://github.com/Vendicated/Vencord.git synced 2025-01-11 02:16:23 +00:00

Merge branch 'dev' into plugin-irc-colors

This commit is contained in:
V 2024-03-13 22:38:15 +01:00 committed by GitHub
commit 949a81d90b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
108 changed files with 1535 additions and 676 deletions

View file

@ -62,7 +62,7 @@ function GM_fetch(url, opt) {
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
resp.text = () => blobTo("text", blob);
resp.json = async () => JSON.parse(await blobTo("text", blob));
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
resp.headers = parseHeaders(resp.responseHeaders);
resp.ok = resp.status >= 200 && resp.status < 300;
resolve(resp);
};

View file

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

View file

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

View file

@ -428,10 +428,11 @@ function runTime(token: string) {
if (searchType === "findComponent") method = "find";
if (searchType === "findExportedComponent") method = "findByProps";
if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") {
if (searchType === "waitFor" || searchType === "waitForComponent") {
if (typeof args[0] === "string") method = "findByProps";
else method = "find";
}
if (searchType === "waitForStore") method = "findStore";
try {
let result: any;

View file

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

View file

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

View file

@ -74,7 +74,7 @@ export interface MessageExtra {
}
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
const sendListeners = new Set<SendListener>();
const editListeners = new Set<EditListener>();
@ -84,7 +84,7 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
for (const listener of sendListeners) {
try {
const result = await listener(channelId, messageObj, extra);
if (result && result.cancel === true) {
if (result?.cancel) {
return true;
}
} catch (e) {
@ -97,11 +97,15 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
for (const listener of editListeners) {
try {
await listener(channelId, messageId, messageObj);
const result = await listener(channelId, messageId, messageObj);
if (result?.cancel) {
return true;
}
} catch (e) {
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
}
}
return false;
}
/**

View file

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

View file

@ -39,9 +39,7 @@ function validateUrl(url: string) {
async function eraseAllData() {
const res = await fetch(new URL("/v1/", getCloudUrl()), {
method: "DELETE",
headers: new Headers({
Authorization: await getCloudAuth()
})
headers: { Authorization: await getCloudAuth() }
});
if (!res.ok) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,10 +25,13 @@ export default definePlugin({
authors: [Devs.Arjix, Devs.hunt, Devs.Ven],
patches: [
{
find: '"MessageActionCreators"',
find: ".Messages.EDIT_TEXTAREA_HELP",
replacement: {
match: /async editMessage\(.+?\)\{/,
replace: "$&await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
match: /(?<=,channel:\i\}\)\.then\().+?(?=return \i\.content!==this\.props\.message\.content&&\i\((.+?)\))/,
replace: (match, args) => "" +
`async ${match}` +
`if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` +
"return Promise.resolve({shoudClear:true,shouldRefocus:true});"
}
},
{

View file

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

View file

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

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({
name: "BetterUploadButton",
authors: [Devs.obscurity, Devs.Ven],
authors: [Devs.fawn, Devs.Ven],
description: "Upload with a single click, open menu with right click",
patches: [
{

View file

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

View file

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

View file

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

View file

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

View file

@ -72,7 +72,7 @@ export default definePlugin({
replacement: [
// Add Decor avatar decoration hook to avatar decoration hook
{
match: /(?<=TryItOut:\i}\),)(?<=user:(\i).+?)/,
match: /(?<=TryItOut:\i,guildId:\i}\),)(?<=user:(\i).+?)/,
replace: "vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1),"
},
// Use added hook
@ -131,9 +131,10 @@ export default definePlugin({
getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {
// Only Decor avatar decorations have this SKU ID
if (avatarDecoration?.skuId === SKU_ID) {
const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`);
url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString());
return url.toString();
const parts = avatarDecoration.asset.split("_");
// Remove a_ prefix if it's animated and animation is disabled
if (isAnimatedAvatarDecoration(avatarDecoration.asset) && !canAnimate) parts.shift();
return `${CDN_URL}/${parts.join("_")}.png`;
} else if (avatarDecoration?.skuId === RAW_SKU_ID) {
return avatarDecoration.asset;
}

View file

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

View file

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

View file

@ -17,14 +17,14 @@
*/
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
@ -51,8 +51,6 @@ const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsA
const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
const USE_EXTERNAL_EMOJIS = 1n << 18n;
const USE_EXTERNAL_STICKERS = 1n << 37n;
const enum EmojiIntentions {
REACTION = 0,
@ -162,12 +160,32 @@ const settings = definePluginSettings({
description: "Whether to use hyperlinks when sending fake emojis and stickers",
type: OptionType.BOOLEAN,
default: true
},
hyperLinkText: {
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.",
type: OptionType.STRING,
default: "{{NAME}}"
}
});
}).withPrivateSettings<{
disableEmbedPermissionCheck: boolean;
}>();
function hasPermission(channelId: string, permission: bigint) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isPrivate()) return true;
return PermissionStore.can(permission, channel);
}
const hasExternalEmojiPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_EMOJIS);
const hasExternalStickerPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_STICKERS);
const hasEmbedPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.EMBED_LINKS);
const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.ATTACH_FILES);
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
dependencies: ["MessageEventsAPI"],
@ -351,8 +369,8 @@ export default definePlugin({
predicate: () => settings.store.transformEmojis,
replacement: {
// Add the fake nitro emoji notice
match: /(?<=isDiscoverable:\i,emojiComesFromCurrentGuild:\i,.+?}=(\i).+?;)(.*?return )(.{0,1000}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(?=},)/,
replace: (_, props, rest, reactNode) => `let{fakeNitroNode}=${props};${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!fakeNitroNode?.fake)`
match: /(?<=emojiDescription:)(\i)(?<=\1=\i\((\i)\).+?)/,
replace: (_, reactNode, props) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!${props}?.fakeNitroNode?.fake)`
}
},
// Allow using custom app icons
@ -456,7 +474,7 @@ export default definePlugin({
if (typeof firstContent === "string") {
content[0] = firstContent.trimStart();
content[0] || content.shift();
} else if (firstContent?.type === "span") {
} else if (typeof firstContent?.props?.children === "string") {
firstContent.props.children = firstContent.props.children.trimStart();
firstContent.props.children || content.shift();
}
@ -466,7 +484,7 @@ export default definePlugin({
if (typeof lastContent === "string") {
content[lastIndex] = lastContent.trimEnd();
content[lastIndex] || content.pop();
} else if (lastContent?.type === "span") {
} else if (typeof lastContent?.props?.children === "string") {
lastContent.props.children = lastContent.props.children.trimEnd();
lastContent.props.children || content.pop();
}
@ -567,13 +585,15 @@ export default definePlugin({
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
children = this.clearEmptyArrayItems(children);
this.trimContent(children);
return children;
};
try {
return modifyChildren(lodash.cloneDeep(content));
const newContent = modifyChildren(lodash.cloneDeep(content));
this.trimContent(newContent);
return newContent;
} catch (err) {
new Logger("FakeNitro").error(err);
return content;
@ -696,22 +716,6 @@ export default definePlugin({
}
},
hasPermissionToUseExternalEmojis(channelId: string): boolean {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel);
},
hasPermissionToUseExternalStickers(channelId: string) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
return PermissionStore.can(USE_EXTERNAL_STICKERS, channel);
},
getStickerLink(stickerId: string) {
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${settings.store.stickerSize}`;
},
@ -722,7 +726,7 @@ export default definePlugin({
const { frames, width, height } = await parseURL(stickerLink);
const gif = GIFEncoder();
const resolution = Settings.plugins.FakeNitro.stickerSize;
const resolution = settings.store.stickerSize;
const canvas = document.createElement("canvas");
canvas.width = resolution;
@ -783,9 +787,38 @@ export default definePlugin({
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
}
this.preSend = addPreSendListener((channelId, messageObj, extra) => {
function cannotEmbedNotice() {
return new Promise<boolean>(resolve => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
You are trying to send/edit a message that contains a FakeNitro emoji or sticker,
however you do not have permissions to embed links in the current channel.
Are you sure you want to send this message? Your FakeNitro items will appear as a link only.
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
You can disable this notice in the plugin settings.
</Forms.FormText>
</div>,
confirmText: "Send Anyway",
cancelText: "Cancel",
secondaryConfirmText: "Do not show again",
onConfirm: () => resolve(true),
onCloseCallback: () => setImmediate(() => resolve(false)),
onConfirmSecondary() {
settings.store.disableEmbedPermissionCheck = true;
resolve(true);
}
});
});
}
this.preSend = addPreSendListener(async (channelId, messageObj, extra) => {
const { guildId } = this;
let hasBypass = false;
stickerBypass: {
if (!s.enableStickerBypass)
break stickerBypass;
@ -798,7 +831,7 @@ export default definePlugin({
if ("pack_id" in sticker)
break stickerBypass;
const canUseStickers = this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId);
const canUseStickers = this.canUseStickers && hasExternalStickerPerms(channelId);
if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId))
break stickerBypass;
@ -812,47 +845,76 @@ export default definePlugin({
}
if (sticker.format_type === StickerType.APNG) {
this.sendAnimatedSticker(link, sticker.id, channelId);
if (!hasAttachmentPerms(channelId)) {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
You cannot send this message because it contains an animated FakeNitro sticker,
and you do not have permissions to attach files in the current channel. Please remove the sticker to proceed.
</Forms.FormText>
</div>
});
} else {
this.sendAnimatedSticker(link, sticker.id, channelId);
}
return { cancel: true };
} else {
hasBypass = true;
const url = new URL(link);
url.searchParams.set("name", sticker.name);
messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${sticker.name}](${url})` : url}`;
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", sticker.name);
messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`;
extra.stickers!.length = 0;
}
}
if (s.enableEmojiBypass) {
const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId);
const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId);
for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue;
if (emoji.available !== false && canUseEmotes) continue;
if (emoji.guildId === guildId && !emoji.animated) continue;
hasBypass = true;
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
const url = new URL(emoji.url);
url.searchParams.set("size", s.emojiSize.toString());
url.searchParams.set("name", emoji.name);
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", emoji.name);
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`;
return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`;
});
}
}
if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {
if (!await cannotEmbedNotice()) {
return { cancel: true };
}
}
return { cancel: false };
});
this.preEdit = addPreEditListener((channelId, __, messageObj) => {
this.preEdit = addPreEditListener(async (channelId, __, messageObj) => {
if (!s.enableEmojiBypass) return;
const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId);
const { guildId } = this;
let hasBypass = false;
const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId);
messageObj.content = messageObj.content.replace(/(?<!\\)<a?:(?:\w+):(\d+)>/ig, (emojiStr, emojiId, offset, origStr) => {
const emoji = EmojiStore.getCustomEmojiById(emojiId);
if (emoji == null) return emojiStr;
@ -860,12 +922,24 @@ export default definePlugin({
if (emoji.available !== false && canUseEmotes) return emojiStr;
if (emoji.guildId === guildId && !emoji.animated) return emojiStr;
hasBypass = true;
const url = new URL(emoji.url);
url.searchParams.set("size", s.emojiSize.toString());
url.searchParams.set("name", emoji.name);
return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`;
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", emoji.name);
return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`;
});
if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {
if (!await cannotEmbedNotice()) {
return { cancel: true };
}
}
return { cancel: false };
});
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,9 @@
box-shadow: inset 0 0 10px 2px grey;
filter: drop-shadow(0 0 2px grey);
pointer-events: none;
/* negate the border offsetting the lens */
margin: -2px;
}
.vc-imgzoom-square {

View file

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

View file

@ -170,6 +170,11 @@ const settings = definePluginSettings({
}
],
},
showLastFmLogo: {
description: "show the Last.fm logo by the album cover",
type: OptionType.BOOLEAN,
default: true,
}
});
export default definePlugin({
@ -276,8 +281,10 @@ export default definePlugin({
{
large_image: await getApplicationAsset(largeImage),
large_text: trackData.album || undefined,
small_image: await getApplicationAsset("lastfm-small"),
small_text: "Last.fm",
...(settings.store.showLastFmLogo && {
small_image: await getApplicationAsset("lastfm-small"),
small_text: "Last.fm"
}),
} : {
large_image: await getApplicationAsset("lastfm-large"),
large_text: trackData.album || undefined,

View file

@ -0,0 +1,66 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { getCurrentChannel } from "@utils/discord";
import { SelectedChannelStore, Tooltip, useEffect, useStateFromStores } from "@webpack/common";
import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat } from ".";
import { OnlineMemberCountStore } from "./OnlineMemberCountStore";
export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) {
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const guildId = isTooltip ? tooltipGuildId! : currentChannel.guild_id;
const totalCount = useStateFromStores(
[GuildMemberCountStore],
() => GuildMemberCountStore.getMemberCount(guildId)
);
let onlineCount = useStateFromStores(
[OnlineMemberCountStore],
() => OnlineMemberCountStore.getCount(guildId)
);
const { groups } = useStateFromStores(
[ChannelMemberStore],
() => ChannelMemberStore.getProps(guildId, currentChannel.id)
);
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {
onlineCount = groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0);
}
useEffect(() => {
OnlineMemberCountStore.ensureCount(guildId);
}, [guildId]);
if (totalCount == null)
return null;
const formattedOnlineCount = onlineCount != null ? numberFormat(onlineCount) : "?";
return (
<div className={cl("widget", { tooltip: isTooltip, "member-list": !isTooltip })}>
<Tooltip text={`${formattedOnlineCount} online in this channel`} position="bottom">
{props => (
<div {...props}>
<span className={cl("online-dot")} />
<span className={cl("online")}>{formattedOnlineCount}</span>
</div>
)}
</Tooltip>
<Tooltip text={`${numberFormat(totalCount)} total server members`} position="bottom">
{props => (
<div {...props}>
<span className={cl("total-dot")} />
<span className={cl("total")}>{numberFormat(totalCount)}</span>
</div>
)}
</Tooltip>
</div>
);
}

View file

@ -0,0 +1,52 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { proxyLazy } from "@utils/lazy";
import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue";
import { Flux, FluxDispatcher, GuildChannelStore, PrivateChannelsStore } from "@webpack/common";
export const OnlineMemberCountStore = proxyLazy(() => {
const preloadQueue = new Queue();
const onlineMemberMap = new Map<string, number>();
class OnlineMemberCountStore extends Flux.Store {
getCount(guildId: string) {
return onlineMemberMap.get(guildId);
}
async _ensureCount(guildId: string) {
if (onlineMemberMap.has(guildId)) return;
await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id);
}
ensureCount(guildId: string) {
if (onlineMemberMap.has(guildId)) return;
preloadQueue.push(() =>
this._ensureCount(guildId)
.then(
() => sleep(200),
() => sleep(200)
)
);
}
}
return new OnlineMemberCountStore(FluxDispatcher, {
GUILD_MEMBER_LIST_UPDATE({ guildId, groups }: { guildId: string, groups: { count: number; id: string; }[]; }) {
onlineMemberMap.set(
guildId,
groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0)
);
},
ONLINE_GUILD_MEMBER_COUNT_UPDATE({ guildId, count }) {
onlineMemberMap.set(guildId, count);
}
});
});

View file

@ -16,101 +16,66 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import definePlugin from "@utils/types";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
import { SelectedChannelStore, Tooltip, useStateFromStores } from "@webpack/common";
import { FluxStore } from "@webpack/types";
const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; };
const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
import { MemberCount } from "./MemberCount";
export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; };
export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
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 numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
function MemberCount() {
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const { groups } = useStateFromStores(
[ChannelMemberStore],
() => ChannelMemberStore.getProps(guildId, channelId)
);
const total = useStateFromStores(
[GuildMemberCountStore],
() => GuildMemberCountStore.getMemberCount(guildId)
);
if (total == null)
return null;
const online =
(groups.length === 1 && groups[0].id === "unknown")
? 0
: groups.reduce((count, curr) => count + (curr.id === "offline" ? 0 : curr.count), 0);
return (
<Flex id="vc-membercount" style={{
marginTop: "1em",
paddingInline: "1em",
justifyContent: "center",
alignContent: "center",
gap: 0
}}>
<Tooltip text={`${numberFormat(online)} online in this channel`} position="bottom">
{props => (
<div {...props}>
<span
style={{
backgroundColor: "var(--green-360)",
width: "12px",
height: "12px",
borderRadius: "50%",
display: "inline-block",
marginRight: "0.5em"
}}
/>
<span style={{ color: "var(--green-360)" }}>{numberFormat(online)}</span>
</div>
)}
</Tooltip>
<Tooltip text={`${numberFormat(total)} total server members`} position="bottom">
{props => (
<div {...props}>
<span
style={{
width: "6px",
height: "6px",
borderRadius: "50%",
border: "3px solid var(--primary-400)",
display: "inline-block",
marginRight: "0.5em",
marginLeft: "1em"
}}
/>
<span style={{ color: "var(--primary-400)" }}>{numberFormat(total)}</span>
</div>
)}
</Tooltip>
</Flex>
);
}
export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
export const cl = classNameFactory("vc-membercount-");
export default definePlugin({
name: "MemberCount",
description: "Shows the amount of online & total members in the server member list",
description: "Shows the amount of online & total members in the server member list and tooltip",
authors: [Devs.Ven, Devs.Commandtechno],
settings,
patches: [{
find: "{isSidebarVisible:",
replacement: {
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: ":[$1?.startsWith('members')?$self.render():null,$2"
patches: [
{
find: "{isSidebarVisible:",
replacement: {
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: ":[$1?.startsWith('members')?$self.render():null,$2"
},
predicate: () => settings.store.memberList
},
{
find: ".invitesDisabledTooltip",
replacement: {
match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/,
replace: ",$self.renderTooltip(arguments[0].guild)]"
},
predicate: () => settings.store.toolTip
}
}],
render: ErrorBoundary.wrap(MemberCount, { noop: true })
],
render: ErrorBoundary.wrap(MemberCount, { noop: true }),
renderTooltip: ErrorBoundary.wrap(guild => <MemberCount isTooltip tooltipGuildId={guild.id} />, { noop: true })
});

View file

@ -0,0 +1,44 @@
.vc-membercount-widget {
display: flex;
align-content: center;
--color-online: var(--green-360);
--color-total: var(--primary-400);
}
.vc-membercount-tooltip {
margin-top: 0.25em;
margin-left: 2px;
}
.vc-membercount-member-list {
justify-content: center;
margin-top: 1em;
padding-inline: 1em;
}
.vc-membercount-online {
color: var(--color-online);
}
.vc-membercount-total {
color: var(--color-total);
}
.vc-membercount-online-dot {
background-color: var(--color-online);
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 0.5em;
}
.vc-membercount-total-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
border: 3px solid var(--color-total);
margin: 0 0.5em 0 1em;
}

View file

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

View file

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

View file

@ -16,16 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/Settings";
import { definePluginSettings,migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
const { updateGuildNotificationSettings } = findByPropsLazy("updateGuildNotificationSettings");
const { toggleShowAllChannels } = findByPropsLazy("toggleShowAllChannels");
const { isOptInEnabledForGuild } = findByPropsLazy("isOptInEnabledForGuild");
const settings = definePluginSettings({
guild: {
description: "Mute Guild",
description: "Mute Guild automatically",
type: OptionType.BOOLEAN,
default: true
},
@ -38,13 +40,20 @@ const settings = definePluginSettings({
description: "Suppress All Role @mentions",
type: OptionType.BOOLEAN,
default: true
},
showAllChannels: {
description: "Show all channels automatically",
type: OptionType.BOOLEAN,
default: true
}
});
migratePluginSettings("NewGuildSettings", "MuteNewGuild");
export default definePlugin({
name: "MuteNewGuild",
description: "Mutes newly joined guilds",
authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince],
name: "NewGuildSettings",
description: "Automatically mute new servers and change various other settings upon joining",
tags: ["MuteNewGuild", "mute", "server"],
authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi],
patches: [
{
find: ",acceptInvite(",
@ -70,7 +79,9 @@ export default definePlugin({
muted: settings.store.guild,
suppress_everyone: settings.store.everyone,
suppress_roles: settings.store.role
}
);
});
if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) {
toggleShowAllChannels(guildId);
}
}
});

View file

@ -27,8 +27,8 @@ export default definePlugin({
{
find: "_ensureAudio(){",
replacement: {
match: /onloadeddata=\(\)=>\{.\.volume=/,
replace: "$&$self.settings.store.notificationVolume/100*"
match: /(?=Math\.min\(\i\.\i\.getOutputVolume\(\)\/100)/,
replace: "$self.settings.store.notificationVolume/100*"
},
},
],

View file

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

View file

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

View file

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

View file

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

View file

@ -20,18 +20,16 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Channel } from "discord-types/general";
import { addContextMenus, removeContextMenus } from "./contextMenus";
import { contextMenus } from "./contextMenus";
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings";
export default definePlugin({
name: "PinDMs",
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
authors: [Devs.Ven, Devs.Strencher],
authors: [Devs.Ven],
settings,
start: addContextMenus,
stop: removeContextMenus,
contextMenus,
usePinCount(channelIds: string[]) {
const pinnedDms = usePinnedDms();

View file

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

View file

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

View file

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

View file

@ -59,7 +59,7 @@ export function authorize(callback?: any) {
const url = new URL(response.location);
url.searchParams.append("clientMod", "vencord");
const res = await fetch(url, {
headers: new Headers({ Accept: "application/json" })
headers: { Accept: "application/json" }
});
if (!res.ok) {

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
import { find, findByPropsLazy } from "@webpack";
import { useAwaiter, useForceUpdater } from "@utils/react";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
import { Auth, authorize } from "../auth";
@ -31,7 +31,8 @@ import ReviewComponent from "./ReviewComponent";
const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms");
const { ChatInputTypes } = findByPropsLazy("ChatInputTypes");
const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default);
const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA");
const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer");
interface UserProps {
discordId: string;
@ -125,19 +126,7 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: {
const inputType = ChatInputTypes.FORM;
inputType.disableAutoFocus = true;
const channel = {
flags_: 256,
guild_id_: null,
id: "0",
getGuildId: () => null,
isPrivate: () => true,
isActiveThread: () => false,
isArchivedLockedThread: () => false,
isDM: () => true,
roles: { "0": { permissions: 0n } },
getRecipientId: () => "0",
hasFlag: () => false,
};
const channel = createChannelRecordFromServer({ id: "0", type: 1 });
return (
<>

View file

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

View file

@ -118,10 +118,10 @@ export async function addReview(review: any): Promise<Response | null> {
export async function deleteReview(id: number): Promise<Response | null> {
return await rdbRequest(`/users/${id}/reviews`, {
method: "DELETE",
headers: new Headers({
headers: {
"Content-Type": "application/json",
Accept: "application/json",
}),
},
body: JSON.stringify({
reviewid: id
})
@ -135,10 +135,10 @@ export async function deleteReview(id: number): Promise<Response | null> {
export async function reportReview(id: number) {
const res = await rdbRequest("/reports", {
method: "PUT",
headers: new Headers({
headers: {
"Content-Type": "application/json",
Accept: "application/json",
}),
},
body: JSON.stringify({
reviewid: id,
})
@ -150,10 +150,10 @@ export async function reportReview(id: number) {
async function patchBlock(action: "block" | "unblock", userId: string) {
const res = await rdbRequest("/blocks", {
method: "PATCH",
headers: new Headers({
headers: {
"Content-Type": "application/json",
Accept: "application/json",
}),
},
body: JSON.stringify({
action: action,
discordId: userId
@ -180,9 +180,9 @@ export const unblockUser = (userId: string) => patchBlock("unblock", userId);
export async function fetchBlocks(): Promise<ReviewDBUser[]> {
const res = await rdbRequest("/blocks", {
method: "GET",
headers: new Headers({
headers: {
Accept: "application/json",
})
}
});
if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -305,27 +305,27 @@ export default definePlugin({
]
},
{
find: ".avatars),children",
find: '+1]})},"overflow"))',
replacement: [
{
// Create a variable for the channel prop
match: /maxUsers:\i,users:\i.+?=(\i).+?;/,
match: /maxUsers:\i,users:\i.+?}=(\i).*?;/,
replace: (m, props) => `${m}let{shcChannel}=${props};`
},
{
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
match: /\i>0(?=&&.{0,60}renderPopout)/,
replace: m => `($self.isHiddenChannel(shcChannel,true)?true:${m})`
replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})`
},
{
// Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
replace: (_, amount) => `($self.isHiddenChannel(shcChannel,true)&&${amount}<=0?0:1)`
replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?0:1)`
},
{
// Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
match: /(?<="\+",)(\i)\+1/,
replace: (m, amount) => `$self.isHiddenChannel(shcChannel,true)&&${amount}<=0?"":${m}`
replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${m}`
}
]
},

View file

@ -21,7 +21,7 @@ import "./spotifyStyles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { debounce } from "@utils/debounce";
import { debounce } from "@shared/debounce";
import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc";
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
@ -371,6 +371,10 @@ export function Player() {
if (!track || !device?.is_active || shouldHide)
return null;
const exportTrackImageStyle = {
"--vc-spotify-track-image": `url(${track?.album?.image?.url || ""})`,
} as React.CSSProperties;
return (
<ErrorBoundary fallback={() => (
<div className="vc-spotify-fallback">
@ -378,7 +382,7 @@ export function Player() {
<p >Check the console for errors</p>
</div>
)}>
<div id={cl("player")}>
<div id={cl("player")} style={exportTrackImageStyle}>
<Info track={track} />
<SeekBar />
<Controls />

View file

@ -31,7 +31,7 @@ function toggleHoverControls(value: boolean) {
export default definePlugin({
name: "SpotifyControls",
description: "Adds a Spotify player above the account panel",
authors: [Devs.Ven, Devs.afn, Devs.KraXen72],
authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000],
options: {
hoverControls: {
description: "Show controls on hover",

View file

@ -170,9 +170,16 @@
/* these importants are necessary, it applies a width and height through inline styles */
height: 10px !important;
width: 10px !important;
margin-top: 4px;
background-color: var(--interactive-normal);
border-color: var(--interactive-normal);
color: var(--interactive-normal);
opacity: 0;
transition: opacity 0.1s;
}
#vc-spotify-progress-bar:hover > [class^="slider"] [class^="grabber"] {
opacity: 1;
}
#vc-spotify-progress-text {

View file

@ -7,6 +7,7 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { UserStore } from "@webpack/common";
export const settings = definePluginSettings({
superReactByDefault: {
@ -49,7 +50,7 @@ export default definePlugin({
find: ".trackEmojiSearchEmpty,200",
replacement: {
match: /(\.trackEmojiSearchEmpty,200(?=.+?isBurstReaction:(\i).+?(\i===\i\.EmojiIntention.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/,
replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.settings.store.superReactByDefault&&${isReactionIntention})`
replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})`
}
}
],
@ -59,5 +60,9 @@ export default definePlugin({
if (settings.store.unlimitedSuperReactionPlaying) return true;
if (playingCount <= settings.store.superReactionPlayingLimit) return true;
return false;
},
get shouldSuperReactByDefault() {
return settings.store.superReactByDefault && UserStore.getCurrentUser().premiumType != null;
}
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,15 +18,17 @@
import "./styles.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Microphone } from "@components/Icons";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types";
import { chooseFile } from "@utils/web";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react";
import { VoiceRecorderDesktop } from "./DesktopRecorder";
@ -46,18 +48,30 @@ export type VoiceRecorder = ComponentType<{
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({
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",
authors: [Devs.Ven, Devs.Vap, Devs.Nickyux],
settings,
start() {
addContextMenuPatch("channel-attach", ctxMenuPatch);
},
stop() {
removeContextMenuPatch("channel-attach", ctxMenuPatch);
contextMenus: {
"channel-attach": ctxMenuPatch
}
});
@ -164,6 +178,11 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) {
fallbackValue: EMPTY_META,
});
const isUnsupportedFormat = blob && (
!blob.type.startsWith("audio/ogg")
|| blob.type.includes("codecs") && !blob.type.includes("opus")
);
return (
<ModalRoot {...modalProps}>
<ModalHeader>
@ -200,6 +219,16 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) {
recording={isRecording}
/>
{isUnsupportedFormat && (
<Card className={`vc-plugins-restart-card ${Margins.top16}`}>
<Forms.FormText>Voice Messages have to be OggOpus to be playable on iOS. This file is <code>{blob.type}</code> so it will not be playable on iOS.</Forms.FormText>
<Forms.FormText className={Margins.top8}>
To fix it, first convert it to OggOpus, for example using the <Link href="https://convertio.co/mp3-opus/">convertio web converter</Link>
</Forms.FormText>
</Card>
)}
</ModalContent>
<ModalFooter>
@ -217,20 +246,3 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) {
</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

@ -69,14 +69,14 @@ function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) {
function makeRenderMoreUsers(users: User[]) {
return function renderMoreUsers(_label: string, _count: number) {
return (
<Tooltip text={users.slice(5).map(u => u.username).join(", ")} >
<Tooltip text={users.slice(4).map(u => u.username).join(", ")} >
{({ onMouseEnter, onMouseLeave }) => (
<div
className={AvatarStyles.moreUsers}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
+{users.length - 5}
+{users.length - 4}
</div>
)}
</Tooltip >

View file

@ -196,7 +196,7 @@ export default definePlugin({
if (message.mention_roles.length > 0) {
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;
const roleColor = role.colorString ?? `#${pingColor}`;
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/>.
*/
import { debounce } from "@utils/debounce";
import { debounce } from "@shared/debounce";
import { contextBridge, webFrame } from "electron";
import { readFileSync, watch } from "fs";
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

@ -106,7 +106,7 @@ export async function authorizeCloud() {
try {
const res = await fetch(location, {
headers: new Headers({ Accept: "application/json" })
headers: { Accept: "application/json" }
});
const { secret } = await res.json();
if (secret) {

View file

@ -16,17 +16,8 @@
* 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 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 interface Dev {
@ -66,8 +57,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "botato",
id: 440990343899643943n
},
obscurity: {
name: "obscurity",
fawn: {
name: "fawn",
id: 336678828233588736n,
},
rushii: {
@ -291,10 +282,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "RyanCaoDev",
id: 952235800110694471n,
},
Strencher: {
name: "Strencher",
id: 415849376598982656n
},
FieryFlames: {
name: "Fiery",
id: 890228870559698955n
@ -399,6 +386,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "maisy",
id: 257109471589957632n,
},
Mopi: {
name: "Mopi",
id: 1022189106614243350n
},
Grzesiek11: {
name: "Grzesiek11",
id: 368475654662127616n,
@ -410,6 +401,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
coolelectronics: {
name: "coolelectronics",
id: 696392247205298207n,
},
Av32000: {
name: "Av32000",
id: 593436735380127770n,
},
Kyuuhachi: {
name: "Kyuuhachi",
id: 236588665420251137n,
},
Elvyra: {
name: "Elvyra",
id: 708275751816003615n,
}
} satisfies Record<string, Dev>);

View file

@ -16,9 +16,10 @@
* 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 "./constants";
export * from "./debounce";
export * from "./discord";
export * from "./guards";
export * from "./lazy";
@ -27,7 +28,6 @@ export * from "./Logger";
export * from "./margins";
export * from "./misc";
export * from "./modal";
export * from "./onceDefined";
export * from "./onlyOnce";
export * from "./patches";
export * from "./Queue";

View file

@ -16,7 +16,7 @@
* 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;
@ -81,10 +81,10 @@ document.addEventListener("DOMContentLoaded", () => {
initThemes();
toggle(Settings.useQuickCss);
addSettingsListener("useQuickCss", toggle);
SettingsStore.addChangeListener("useQuickCss", toggle);
addSettingsListener("themeLinks", initThemes);
addSettingsListener("enabledThemes", initThemes);
SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes);
if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes);

View file

@ -36,14 +36,14 @@ export async function importSettings(data: string) {
if ("settings" in parsed && "quickCss" in parsed) {
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);
} else
throw new Error("Invalid Settings. Is this even a Vencord Settings file?");
}
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();
return JSON.stringify({ settings, quickCss }, null, minify ? undefined : 4);
}
@ -118,10 +118,10 @@ export async function putCloudSettings(manual?: boolean) {
try {
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
method: "PUT",
headers: new Headers({
headers: {
Authorization: await getCloudAuth(),
"Content-Type": "application/octet-stream"
}),
},
body: deflateSync(new TextEncoder().encode(settings))
});
@ -137,7 +137,7 @@ export async function putCloudSettings(manual?: boolean) {
const { written } = await res.json();
PlainSettings.cloud.settingsSyncVersion = written;
VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4));
VencordNative.settings.set(PlainSettings);
cloudSettingsLogger.info("Settings uploaded to cloud successfully");
@ -162,11 +162,11 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
try {
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
method: "GET",
headers: new Headers({
headers: {
Authorization: await getCloudAuth(),
Accept: "application/octet-stream",
"If-None-Match": Settings.cloud.settingsSyncVersion.toString()
}),
},
});
if (res.status === 404) {
@ -222,7 +222,7 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
// sync with server timestamp instead of local one
PlainSettings.cloud.settingsSyncVersion = written;
VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4));
VencordNative.settings.set(PlainSettings);
cloudSettingsLogger.info("Settings loaded from cloud successfully");
if (shouldNotify)
@ -251,9 +251,7 @@ export async function deleteCloudSettings() {
try {
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
method: "DELETE",
headers: new Headers({
Authorization: await getCloudAuth()
}),
headers: { Authorization: await getCloudAuth() },
});
if (!res.ok) {

View file

@ -17,6 +17,7 @@
*/
import { Command } from "@api/Commands";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { FluxEvents } from "@webpack/types";
import { Promisable } from "type-fest";
@ -115,6 +116,10 @@ export interface PluginDef {
flux?: {
[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.
* 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 */
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 Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);

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