Merge branch 'dev' into remix

This commit is contained in:
MrDiamondDog 2024-03-21 17:49:46 -06:00 committed by GitHub
commit dc2b61a98d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
123 changed files with 2070 additions and 886 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.7",
"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

@ -21,7 +21,7 @@ import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react";
@ -129,7 +129,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
richBody={
<div className={cl("body")}>
{data.body}
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
</div>
}
/>

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,7 +25,6 @@ import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater";
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
import type { ReactElement } from "react";
const CrashHandlerLogger = new Logger("CrashHandler");
const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
@ -57,13 +56,13 @@ const settings = definePluginSettings({
}
});
let crashCount: number = 0;
let lastCrashTimestamp: number = 0;
let shouldAttemptNextHandle = false;
let hasCrashedOnce = false;
let isRecovering = false;
let shouldAttemptRecover = true;
export default definePlugin({
name: "CrashHandler",
description: "Utility plugin for handling and possibly recovering from Crashes without a restart",
description: "Utility plugin for handling and possibly recovering from crashes without a restart",
authors: [Devs.Nuckyz],
enabledByDefault: true,
@ -73,61 +72,67 @@ export default definePlugin({
{
find: ".Messages.ERRORS_UNEXPECTED_CRASH",
replacement: {
match: /(?=this\.setState\()/,
replace: "$self.handleCrash(this)||"
match: /this\.setState\((.+?)\)/,
replace: "$self.handleCrash(this,$1);"
}
}
],
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true;
handleCrash(_this: any, errorState: any) {
_this.setState(errorState);
shouldAttemptNextHandle = false;
// Already recovering, prevent error which happens more than once too fast to trigger another recover
if (isRecovering) return;
isRecovering = true;
if (++crashCount > 5) {
// 1 ms timeout to avoid react breaking when re-rendering
setTimeout(() => {
try {
showNotification({
color: "#eed202",
title: "Discord has crashed!",
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
noPersist: true,
});
// Prevent a crash loop with an error that could not be handled
if (!shouldAttemptRecover) {
try {
showNotification({
color: "#eed202",
title: "Discord has crashed!",
body: "Awn :( Discord has crashed two times rapidly, not attempting to recover.",
noPersist: true
});
} catch { }
return;
}
shouldAttemptRecover = false;
// This is enough to avoid a crash loop
setTimeout(() => shouldAttemptRecover = true, 500);
} catch { }
lastCrashTimestamp = Date.now();
return false;
}
try {
if (!hasCrashedOnce) {
hasCrashedOnce = true;
maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
}
} catch { }
setTimeout(() => crashCount--, 60_000);
try {
if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
if (settings.store.attemptToPreventCrashes) {
this.handlePreventCrash(_this);
return true;
try {
if (settings.store.attemptToPreventCrashes) {
this.handlePreventCrash(_this);
}
} catch (err) {
CrashHandlerLogger.error("Failed to handle crash", err);
}
return false;
} catch (err) {
CrashHandlerLogger.error("Failed to handle crash", err);
return false;
} finally {
lastCrashTimestamp = Date.now();
}
}, 1);
},
handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) {
if (Date.now() - lastCrashTimestamp >= 1_000) {
try {
showNotification({
color: "#eed202",
title: "Discord has crashed!",
body: "Attempting to recover...",
noPersist: true,
});
} catch { }
}
handlePreventCrash(_this: any) {
try {
showNotification({
color: "#eed202",
title: "Discord has crashed!",
body: "Attempting to recover...",
noPersist: true
});
} catch { }
try {
const channelId = SelectedChannelStore.getChannelId();
@ -176,9 +181,12 @@ export default definePlugin({
}
}
// Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens
setImmediate(() => isRecovering = false);
try {
shouldAttemptNextHandle = true;
_this.forceUpdate();
_this.setState({ error: null, info: null });
} catch (err) {
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
}

View file

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

View file

@ -43,7 +43,7 @@ export default definePlugin({
{
find: "DefaultCustomizationSections",
replacement: {
match: /(?<={user:\i},"decoration"\),)/,
match: /(?<=USER_SETTINGS_AVATAR_DECORATION},"decoration"\),)/,
replace: "$self.DecorSection(),"
}
},
@ -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,18 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
migratePluginSettings("DisableCallIdle", "DisableDMCallIdle");
export default definePlugin({
name: "DisableDMCallIdle",
description: "Disables automatically getting kicked from a DM voice call after 3 minutes.",
name: "DisableCallIdle",
description: "Disables automatically getting kicked from a DM voice call after 3 minutes and being moved to an AFK voice channel.",
authors: [Devs.Nuckyz],
patches: [
{
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
replacement: {
match: /(?<=function \i\(\){)(?=.{1,120}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/,
match: /,?(?=this\.idleTimeout=new \i\.Timeout)/,
replace: ";return;"
}
},
{
find: "handleIdleUpdate(){",
replacement: {
match: /(?<=_initialize\(\){)/,
replace: "return;"
}
}

View file

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

View file

@ -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,
@ -108,6 +106,7 @@ const enum FakeNoticeType {
const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
const settings = definePluginSettings({
enableEmojiBypass: {
@ -156,12 +155,37 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
},
useHyperLinks: {
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"],
@ -345,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
@ -447,13 +471,23 @@ export default definePlugin({
trimContent(content: Array<any>) {
const firstContent = content[0];
if (typeof firstContent === "string") content[0] = firstContent.trimStart();
if (content[0] === "") content.shift();
if (typeof firstContent === "string") {
content[0] = firstContent.trimStart();
content[0] || content.shift();
} else if (typeof firstContent?.props?.children === "string") {
firstContent.props.children = firstContent.props.children.trimStart();
firstContent.props.children || content.shift();
}
const lastIndex = content.length - 1;
const lastContent = content[lastIndex];
if (typeof lastContent === "string") content[lastIndex] = lastContent.trimEnd();
if (content[lastIndex] === "") content.pop();
if (typeof lastContent === "string") {
content[lastIndex] = lastContent.trimEnd();
content[lastIndex] || content.pop();
} else if (typeof lastContent?.props?.children === "string") {
lastContent.props.children = lastContent.props.children.trimEnd();
lastContent.props.children || content.pop();
}
},
clearEmptyArrayItems(array: Array<any>) {
@ -465,7 +499,7 @@ export default definePlugin({
},
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
// If content has more than one child or it's a single ReactElement like a header or list
// If content has more than one child or it's a single ReactElement like a header, list or span
if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content;
let nextIndex = content.length;
@ -551,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;
@ -574,7 +610,7 @@ export default definePlugin({
itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url));
for (const item of itemsToMaybePush) {
if (!settings.store.transformCompoundSentence && !item.startsWith("http")) continue;
if (!settings.store.transformCompoundSentence && !item.startsWith("http") && !hyperLinkRegex.test(item)) continue;
const imgMatch = item.match(fakeNitroStickerRegex);
if (imgMatch) {
@ -619,8 +655,7 @@ export default definePlugin({
case "image": {
if (
!settings.store.transformCompoundSentence
&& !contentItems.includes(embed.url!)
&& !contentItems.includes(embed.image?.proxyURL!)
&& !contentItems.some(item => item === embed.url! || item.match(hyperLinkRegex)?.[1] === embed.url!)
) return false;
if (settings.store.transformEmojis) {
@ -681,24 +716,8 @@ 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.plugins.FakeNitro.stickerSize}`;
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${settings.store.stickerSize}`;
},
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
@ -707,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;
@ -768,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;
@ -783,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;
@ -795,44 +843,78 @@ export default definePlugin({
if (sticker.format_type === StickerType.GIF && link.includes(".png")) {
link = link.replace(".png", ".gif");
}
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);
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;
messageObj.content += ` ${link}&name=${encodeURIComponent(sticker.name)}`;
}
}
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 = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({
size: Settings.plugins.FakeNitro.emojiSize,
name: encodeURIComponent(emoji.name)
}));
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)}${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;
@ -840,12 +922,24 @@ export default definePlugin({
if (emoji.available !== false && canUseEmotes) return emojiStr;
if (emoji.guildId === guildId && !emoji.animated) return emojiStr;
const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({
size: Settings.plugins.FakeNitro.emojiSize,
name: encodeURIComponent(emoji.name)
}));
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + emojiStr.length)}`;
hasBypass = true;
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);
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,20 +4,20 @@
* 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(`
new MutationObserver(() => {
if(
document.querySelector('div.ytp-error-content-wrap-subreason a[href^="https://www.youtube.com/watch?v="]')
document.querySelector('div.ytp-error-content-wrap-subreason a[href*="www.youtube.com/watch?v="]')
) location.reload()
}).observe(document.body, { childList: true, subtree:true });
`);

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

@ -123,14 +123,13 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
waitFor(() => instance.state.readyState === "READY", () => {
const elem = document.getElementById(ELEMENT_ID) as HTMLDivElement;
element.current = elem;
elem.firstElementChild!.setAttribute("draggable", "false");
elem.querySelector("img,video")?.setAttribute("draggable", "false");
if (instance.props.animated) {
originalVideoElementRef.current = elem!.querySelector("video")!;
originalVideoElementRef.current.addEventListener("timeupdate", syncVideos);
setReady(true);
} else {
setReady(true);
}
setReady(true);
});
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
@ -155,7 +154,9 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
if (!ready) return null;
const box = element.current!.getBoundingClientRect();
const box = element.current?.getBoundingClientRect();
if (!box) return null;
return (
<div

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
@ -171,7 +171,7 @@ export default definePlugin({
find: "handleImageLoad=",
replacement: [
{
match: /showThumbhashPlaceholder:\i,/,
match: /placeholderVersion:\i,/,
replace: "...$self.makeProps(this),$&"
},
@ -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

@ -29,15 +29,17 @@ import {
ChannelStore,
FluxDispatcher,
GuildStore,
IconUtils,
MessageStore,
Parser,
PermissionsBits,
PermissionStore,
RestAPI,
Text,
TextAndImagesSettingsStores,
UserStore
} from "@webpack/common";
import { Channel, Guild, Message } from "discord-types/general";
import { Channel, Message } from "discord-types/general";
const messageCache = new Map<string, {
message?: Message;
@ -49,8 +51,9 @@ const AutoModEmbed = findComponentByCodeLazy(".withFooter]:", "childrenMessageCo
const ChannelMessage = findComponentByCodeLazy("renderSimpleAccessories)");
const SearchResultClasses = findByPropsLazy("message", "searchResult");
const EmbedClasses = findByPropsLazy("embedAuthorIcon", "embedAuthor", "embedAuthor");
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(?:\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
const tenorRegex = /^https:\/\/(?:www\.)?tenor\.com\//;
interface Attachment {
@ -63,7 +66,6 @@ interface Attachment {
interface MessageEmbedProps {
message: Message;
channel: Channel;
guildID: string;
}
const messageFetchQueue = new Queue();
@ -226,19 +228,19 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
let match = null as RegExpMatchArray | null;
while ((match = messageLinkRegex.exec(message.content!)) !== null) {
const [_, guildID, channelID, messageID] = match;
const [_, channelID, messageID] = match;
if (embeddedBy.includes(messageID)) {
continue;
}
const linkedChannel = ChannelStore.getChannel(channelID);
if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) {
if (!linkedChannel || (!linkedChannel.isPrivate() && !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, linkedChannel))) {
continue;
}
const { listMode, idList } = settings.store;
const isListed = [guildID, channelID, message.author.id].some(id => id && idList.includes(id));
const isListed = [linkedChannel.guild_id, channelID, message.author.id].some(id => id && idList.includes(id));
if (listMode === "blacklist" && isListed) continue;
if (listMode === "whitelist" && !isListed) continue;
@ -265,8 +267,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
const messageProps: MessageEmbedProps = {
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
channel: linkedChannel,
guildID
channel: linkedChannel
};
const type = settings.store.automodEmbeds;
@ -280,59 +281,64 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
return accessories.length ? <>{accessories}</> : null;
}
function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null {
const isDM = guildID === "@me";
function getChannelLabelAndIconUrl(channel: Channel) {
if (channel.isDM()) return ["Direct Message", IconUtils.getUserAvatarURL(UserStore.getUser(channel.recipients[0]))];
if (channel.isGroupDM()) return ["Group DM", IconUtils.getChannelIconURL(channel)];
return ["Server", IconUtils.getGuildIconURL(GuildStore.getGuild(channel.guild_id))];
}
const guild = !isDM && GuildStore.getGuild(channel.guild_id);
function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps): JSX.Element | null {
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);
const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel);
return <Embed
embed={{
rawDescription: "",
color: "var(--background-secondary)",
author: {
name: <Text variant="text-xs/medium" tag="span">
<span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>
{isDM
? Parser.parse(`<@${dmReceiver.id}>`)
: Parser.parse(`<#${channel.id}>`)
}
</Text>,
iconProxyURL: guild
? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png`
: `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}`
}
}}
renderDescription={() => (
<div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
<ChannelMessage
id={`message-link-embeds-${message.id}`}
message={message}
channel={channel}
subscribeToComponentDispatch={false}
/>
</div>
)}
/>;
return (
<Embed
embed={{
rawDescription: "",
color: "var(--background-secondary)",
author: {
name: <Text variant="text-xs/medium" tag="span">
<span>{channelLabel} - </span>
{Parser.parse(channel.isDM() ? `<@${dmReceiver.id}>` : `<#${channel.id}>`)}
</Text>,
iconProxyURL: iconUrl
}
}}
renderDescription={() => (
<div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
<ChannelMessage
id={`message-link-embeds-${message.id}`}
message={message}
channel={channel}
subscribeToComponentDispatch={false}
/>
</div>
)}
/>
);
}
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
const { message, channel, guildID } = props;
const { message, channel } = props;
const compact = TextAndImagesSettingsStores.MessageDisplayCompact.useSetting();
const isDM = guildID === "@me";
const images = getImages(message);
const { parse } = Parser;
const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel);
return <AutoModEmbed
channel={channel}
childrenAccessories={
<Text color="text-muted" variant="text-xs/medium" tag="span">
{isDM
? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
: parse(`<#${channel.id}>`)
}
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
<Text color="text-muted" variant="text-xs/medium" tag="span" className={`${EmbedClasses.embedAuthor} ${EmbedClasses.embedMargin}`}>
{iconUrl && <img src={iconUrl} className={EmbedClasses.embedAuthorIcon} alt="" />}
<span>
<span>{channelLabel} - </span>
{channel.isDM()
? Parser.parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
: Parser.parse(`<#${channel.id}>`)
}
</span>
</Text>
}
compact={compact}

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";
@ -26,7 +26,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, i18n, Menu, moment, Parser, Timestamp, UserStore } from "@webpack/common";
import { ChannelStore, FluxDispatcher, i18n, Menu, Parser, Timestamp, UserStore } from "@webpack/common";
import overlayStyle from "./deleteStyleOverlay.css?managed";
import textStyle from "./deleteStyleText.css?managed";
@ -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; }) {
@ -122,7 +121,7 @@ export default definePlugin({
makeEdit(newMessage: any, oldMessage: any): any {
return {
timestamp: moment?.call(newMessage.edited_timestamp),
timestamp: new Date(newMessage.edited_timestamp),
content: oldMessage.content
};
},
@ -138,6 +137,16 @@ export default definePlugin({
],
onChange: () => addDeleteStyle()
},
logDeletes: {
type: OptionType.BOOLEAN,
description: "Whether to log deleted messages",
default: true,
},
logEdits: {
type: OptionType.BOOLEAN,
description: "Whether to log edited messages",
default: true,
},
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by bots",
@ -198,8 +207,8 @@ export default definePlugin({
return cache;
},
shouldIgnore(message: any) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
shouldIgnore(message: any, isEdit = false) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds, logEdits, logDeletes } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
return ignoreBots && message.author?.bot ||
@ -207,6 +216,7 @@ export default definePlugin({
ignoreUsers.includes(message.author?.id) ||
ignoreChannels.includes(message.channel_id) ||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
(isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
},
@ -242,7 +252,7 @@ export default definePlugin({
match: /(MESSAGE_UPDATE:function\((\i)\).+?)\.update\((\i)/,
replace: "$1" +
".update($3,m =>" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" +
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" +

View file

@ -20,11 +20,10 @@ import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common";
import { Channel, User } from "discord-types/general";
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
const AvatarUtils = findByPropsLazy("getChannelIconURL");
const UserUtils = findByPropsLazy("getGlobalName");
const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds");
@ -48,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"}),'
}
},
{
@ -71,7 +70,7 @@ export default definePlugin({
}}
>
<Avatar
src={AvatarUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
>

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

@ -104,6 +104,7 @@ function UserPermissionsComponent({ guild, guildMember, showBorder }: { guild: G
guildMember.nick || UserStore.getUser(guildMember.userId).username
)
}
onDropDownClick={state => settings.store.defaultPermissionsDropdownState = !state}
defaultState={settings.store.defaultPermissionsDropdownState}
buttons={[
(<Tooltip text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>

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

@ -20,7 +20,7 @@ import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack";
import { Alerts, moment, Parser, Timestamp, useState } from "@webpack/common";
import { Alerts, Parser, Timestamp, useState } from "@webpack/common";
import { Auth, getToken } from "../auth";
import { Review, ReviewType } from "../entities";
@ -163,7 +163,7 @@ export default LazyComponent(() => {
{
!settings.store.hideTimestamps && review.type !== ReviewType.System && (
<Timestamp timestamp={moment(review.timestamp * 1000)} >
<Timestamp timestamp={new Date(review.timestamp * 1000)} >
{dateFormat.format(review.timestamp * 1000)}
</Timestamp>)
}

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", "input");
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,10 +12,9 @@ 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, moment, 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 IconUtils = findByPropsLazy("getGuildBannerURL");
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
const FriendRow = findExportedComponentLazy("FriendRow");
@ -50,7 +49,7 @@ const fetched = {
function renderTimestamp(timestamp: number) {
return (
<Timestamp timestamp={moment(timestamp)} />
<Timestamp timestamp={new Date(timestamp)} />
);
}
@ -65,10 +64,7 @@ function GuildProfileModal({ guild }: GuildProps) {
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
const bannerUrl = guild.banner && IconUtils.getGuildBannerURL({
id: guild.id,
banner: guild.banner
}, true).replace(/\?size=\d+$/, "?size=1024");
const bannerUrl = guild.banner && IconUtils.getGuildBannerURL(guild, true)!.replace(/\?size=\d+$/, "?size=1024");
const iconUrl = guild.icon && IconUtils.getGuildIconURL({
id: guild.id,
@ -89,7 +85,7 @@ function GuildProfileModal({ guild }: GuildProps) {
)}
<div className={cl("header")}>
{guild.icon
{iconUrl
? <img
src={iconUrl}
alt=""
@ -150,7 +146,7 @@ function Owner(guildId: string, owner: User) {
avatar: guildAvatar,
guildId,
canAnimate: true
}, true)
})
: IconUtils.getUserAvatarURL(owner, true);
return (
@ -176,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

@ -20,7 +20,7 @@ import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { formatDuration } from "@utils/text";
import { findByPropsLazy, findComponentByCodeLazy, findComponentLazy } from "@webpack";
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
import type { Channel } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "../../permissionsViewer/components/RolesAndUsersPermissions";
@ -120,7 +120,7 @@ const VideoQualityModesToNames = {
const HiddenChannelLogo = "/assets/433e3ec4319a9d11b0cbe39342614982.svg";
function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
const [viewAllowedUsersAndRoles, setViewAllowedUsersAndRoles] = useState(settings.store.defaultAllowedUsersAndRolesDropdownState);
const { defaultAllowedUsersAndRolesDropdownState } = settings.use(["defaultAllowedUsersAndRolesDropdownState"]);
const [permissions, setPermissions] = useState<RoleOrUserPermission[]>([]);
const {
@ -216,12 +216,12 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
{lastMessageId &&
<Text variant="text-md/normal">
Last {channel.isForumChannel() ? "post" : "message"} created:
<Timestamp timestamp={moment(SnowflakeUtils.extractTimestamp(lastMessageId))} />
<Timestamp timestamp={new Date(SnowflakeUtils.extractTimestamp(lastMessageId))} />
</Text>
}
{lastPinTimestamp &&
<Text variant="text-md/normal">Last message pin: <Timestamp timestamp={moment(lastPinTimestamp)} /></Text>
<Text variant="text-md/normal">Last message pin: <Timestamp timestamp={new Date(lastPinTimestamp)} /></Text>
}
{(rateLimitPerUser ?? 0) > 0 &&
<Text variant="text-md/normal">Slowmode: {formatDuration(rateLimitPerUser!, "seconds")}</Text>
@ -301,19 +301,19 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
</Tooltip>
)}
<Text variant="text-lg/bold">Allowed users and roles:</Text>
<Tooltip text={viewAllowedUsersAndRoles ? "Hide Allowed Users and Roles" : "View Allowed Users and Roles"}>
<Tooltip text={defaultAllowedUsersAndRolesDropdownState ? "Hide Allowed Users and Roles" : "View Allowed Users and Roles"}>
{({ onMouseLeave, onMouseEnter }) => (
<button
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
className="shc-lock-screen-allowed-users-and-roles-container-toggle-btn"
onClick={() => setViewAllowedUsersAndRoles(v => !v)}
onClick={() => settings.store.defaultAllowedUsersAndRolesDropdownState = !defaultAllowedUsersAndRolesDropdownState}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
transform={viewAllowedUsersAndRoles ? "scale(1 -1)" : "scale(1 1)"}
transform={defaultAllowedUsersAndRolesDropdownState ? "scale(1 -1)" : "scale(1 1)"}
>
<path fill="currentColor" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
</svg>
@ -321,7 +321,7 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
)}
</Tooltip>
</div>
{viewAllowedUsersAndRoles && <ChannelBeginHeader channel={channel} />}
{defaultAllowedUsersAndRolesDropdownState && <ChannelBeginHeader channel={channel} />}
</div>
</div>
</div>

View file

@ -29,7 +29,7 @@ import type { Channel, Role } from "discord-types/general";
import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen";
const ChannelListClasses = findByPropsLazy("channelEmoji", "unread", "icon");
const ChannelListClasses = findByPropsLazy("modeMuted", "modeSelected", "unread", "icon");
const enum ShowMode {
LockIcon,
@ -162,7 +162,7 @@ export default definePlugin({
},
// Add the hidden eye icon if the channel is hidden
{
match: /\i\.children.+?:null(?<=,channel:(\i).+?)/,
match: /\.name\),.{0,120}\.children.+?:null(?<=,channel:(\i).+?)/,
replace: (m, channel) => `${m},$self.isHiddenChannel(${channel})?$self.HiddenChannelIcon():null`
},
// Make voice channels also appear as muted if they are muted
@ -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: [
@ -133,7 +133,7 @@ export default definePlugin({
{
find: "UNREAD_IMPORTANT:",
replacement: {
match: /channel:(\i).{0,100}?channelEmoji,.{0,250}?\.children.{0,50}?:null/,
match: /\.name\),.{0,120}\.children.+?:null(?<=,channel:(\i).+?)/,
replace: "$&,$self.TypingIndicator($1.id)"
}
},

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

@ -96,7 +96,7 @@ export default definePlugin({
patches: [
// above message box
{
find: ".lastEditedByContainer",
find: ".popularApplicationCommandIds,",
replacement: {
match: /\(0,\i\.jsx\)\(\i\.\i,{user:\i,setNote/,
replace: "$self.patchPopout(arguments[0]),$&",

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,17 +16,15 @@
* 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";
import { openImageModal } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { GuildMemberStore, Menu } from "@webpack/common";
import { GuildMemberStore, IconUtils, Menu } from "@webpack/common";
import type { Channel, Guild, User } from "discord-types/general";
const BannerStore = findByPropsLazy("getGuildBannerURL");
interface UserContextProps {
channel: Channel;
@ -82,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;
@ -91,19 +89,19 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
<Menu.MenuItem
id="view-avatar"
label="View Avatar"
action={() => openImage(BannerStore.getUserAvatarURL(user, true))}
action={() => openImage(IconUtils.getUserAvatarURL(user, true))}
icon={ImageIcon}
/>
{memberAvatar && (
<Menu.MenuItem
id="view-server-avatar"
label="View Server Avatar"
action={() => openImage(BannerStore.getGuildMemberAvatarURLSimple({
action={() => openImage(IconUtils.getGuildMemberAvatarURLSimple({
userId: user.id,
avatar: memberAvatar,
guildId,
guildId: guildId!,
canAnimate: true
}, true))}
}))}
icon={ImageIcon}
/>
)}
@ -111,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;
@ -124,11 +122,11 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon
id="view-icon"
label="View Icon"
action={() =>
openImage(BannerStore.getGuildIconURL({
openImage(IconUtils.getGuildIconURL({
id,
icon,
canAnimate: true
}))
})!)
}
icon={ImageIcon}
/>
@ -138,10 +136,7 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon
id="view-banner"
label="View Banner"
action={() =>
openImage(BannerStore.getGuildBannerURL({
id,
banner,
}, true))
openImage(IconUtils.getGuildBannerURL(guild, true)!)
}
icon={ImageIcon}
/>
@ -160,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: [
@ -184,7 +174,7 @@ export default definePlugin({
find: ".NITRO_BANNER,",
replacement: {
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
match: /style:\{(?=backgroundImage:(\i&&\i)\?"url\("\.concat\((\i),)/,
match: /style:\{(?=backgroundImage:(\i)\?"url\("\.concat\((\i),)/,
replace:
// onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'

View file

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

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

@ -47,18 +47,23 @@ const settings = definePluginSettings({
});
const MEDIA_PROXY_URL = "https://media.discordapp.net";
const CDN_URL = "https://cdn.discordapp.com";
const CDN_URL = "cdn.discordapp.com";
function fixImageUrl(urlString: string, explodeWebp: boolean) {
function fixImageUrl(urlString: string) {
const url = new URL(urlString);
if (url.origin === CDN_URL) return urlString;
if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname;
if (url.host === CDN_URL) return urlString;
url.searchParams.delete("width");
url.searchParams.delete("height");
url.searchParams.set("quality", "lossless");
if (explodeWebp && url.searchParams.get("format") === "webp")
url.searchParams.set("format", "png");
if (url.origin === MEDIA_PROXY_URL) {
url.host = CDN_URL;
url.searchParams.delete("size");
url.searchParams.delete("quality");
url.searchParams.delete("format");
} else {
url.searchParams.set("quality", "lossless");
}
return url.toString();
}
@ -199,7 +204,7 @@ export default definePlugin({
],
async copyImage(url: string) {
url = fixImageUrl(url, true);
url = fixImageUrl(url);
let imageData = await fetch(url).then(r => r.blob());
if (imageData.type !== "image/png") {
@ -231,7 +236,7 @@ export default definePlugin({
},
async saveImage(url: string) {
url = fixImageUrl(url, false);
url = fixImageUrl(url);
const data = await fetchImage(url);
if (!data) return;

View file

@ -29,7 +29,7 @@ import { Message, ReactionEmoji, User } from "discord-types/general";
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
let Scroll: any = null;
const queue = new Queue();
let reactions: Record<string, ReactionCacheEntry>;
@ -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 >
@ -91,7 +91,7 @@ function handleClickAvatar(event: React.MouseEvent<HTMLElement, MouseEvent>) {
export default definePlugin({
name: "WhoReacted",
description: "Renders the avatars of users who reacted to a message",
authors: [Devs.Ven, Devs.KannaDev],
authors: [Devs.Ven, Devs.KannaDev, Devs.newwares],
patches: [{
find: ",reactionRef:",
@ -105,7 +105,19 @@ export default definePlugin({
match: /(?<=CONNECTION_OPEN:function\(\){)(\i)={}/,
replace: "$&;$self.reactions=$1"
}
}],
},
{
find: "cleanAutomaticAnchor(){",
replacement: {
match: /this\.automaticAnchor=null,this\.messageFetchAnchor=null,/,
replace: "$&$self.setScrollObj(this),"
}
}
],
setScrollObj(scroll: any) {
Scroll = scroll;
},
renderUsers(props: RootObject) {
return props.message.reactions.length > 10 ? null : (
@ -114,9 +126,13 @@ export default definePlugin({
</ErrorBoundary>
);
},
_renderUsers({ message, emoji, type }: RootObject) {
const forceUpdate = useForceUpdater();
React.useLayoutEffect(() => { // bc need to prevent autoscrolling
if (Scroll?.scrollCounter > 0) {
Scroll.setAutomaticAnchor(null);
}
});
React.useEffect(() => {
const cb = (e: any) => {
if (e.messageId === message.id)

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>`);

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