Merge branch 'main' of https://www.coastalcommits.com/CoastalCommitsArchival/github.com-Vendicated-Vencord
This commit is contained in:
commit
3291483d39
74 changed files with 1641 additions and 548 deletions
|
@ -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"
|
||||
},
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.7.2",
|
||||
"version": "1.7.3",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
|
||||
|
|
|
@ -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,45 +192,21 @@ export const Settings = makeProxy(settings);
|
|||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||
|
||||
if (paths) {
|
||||
(forceUpdate as SubscriptionCallback)._paths = paths;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
subscriptions.add(forceUpdate);
|
||||
return () => void subscriptions.delete(forceUpdate);
|
||||
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) {
|
||||
|
@ -269,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
import { ReplaceFn } from "@utils/types";
|
||||
import { Patch, ReplaceFn } from "@utils/types";
|
||||
import { search } from "@webpack";
|
||||
import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common";
|
||||
import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
|
@ -218,6 +218,60 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
);
|
||||
}
|
||||
|
||||
interface FullPatchInputProps {
|
||||
setFind(v: string): void;
|
||||
setMatch(v: string): void;
|
||||
setReplacement(v: string | ReplaceFn): void;
|
||||
}
|
||||
|
||||
function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputProps) {
|
||||
const [fullPatch, setFullPatch] = React.useState<string>("");
|
||||
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
||||
|
||||
function update() {
|
||||
if (fullPatch === "") {
|
||||
setFullPatchError("");
|
||||
|
||||
setFind("");
|
||||
setMatch("");
|
||||
setReplacement("");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = (0, eval)(`(${fullPatch})`) as Patch;
|
||||
|
||||
if (!parsed.find) throw new Error("No 'find' field");
|
||||
if (!parsed.replacement) throw new Error("No 'replacement' field");
|
||||
|
||||
if (parsed.replacement instanceof Array) {
|
||||
if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
|
||||
|
||||
parsed.replacement = {
|
||||
match: parsed.replacement[0].match,
|
||||
replace: parsed.replacement[0].replace
|
||||
};
|
||||
}
|
||||
|
||||
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
||||
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
||||
|
||||
setFind(parsed.find);
|
||||
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
||||
setReplacement(parsed.replacement.replace);
|
||||
setFullPatchError("");
|
||||
} catch (e) {
|
||||
setFullPatchError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<Forms.FormText>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
||||
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
||||
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
||||
</>;
|
||||
}
|
||||
|
||||
function PatchHelper() {
|
||||
const [find, setFind] = React.useState<string>("");
|
||||
const [match, setMatch] = React.useState<string>("");
|
||||
|
@ -260,6 +314,13 @@ function PatchHelper() {
|
|||
|
||||
return (
|
||||
<SettingsTab title="Patch Helper">
|
||||
<Forms.FormTitle>full patch</Forms.FormTitle>
|
||||
<FullPatchInput
|
||||
setFind={onFindChange}
|
||||
setMatch={onMatchChange}
|
||||
setReplacement={setReplacement}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle>find</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 { FSWatcher, 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,13 +98,6 @@ 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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
53
src/main/settings.ts
Normal 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);
|
||||
}
|
||||
});
|
|
@ -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";
|
||||
|
|
|
@ -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)
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -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:",
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { findGroupChildrenByChildId } 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,
|
||||
|
||||
contextMenus: {
|
||||
// The settings shortcuts in the user settings cog context menu
|
||||
// read the elements from a hardcoded map which for obvious reason
|
||||
// doesn't contain our sections. This patches the actions of our
|
||||
// sections to manually use SettingsRouter (which only works on desktop
|
||||
// but the context menu is usually not available on mobile anyway)
|
||||
"user-settings-cog"(children) {
|
||||
const section = findGroupChildrenByChildId("VencordSettings", children);
|
||||
section?.forEach(c => {
|
||||
const id = c?.props?.id;
|
||||
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
|
||||
c!.props.action = () => SettingsRouter.open(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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)[],
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentGuild, getGuildRoles } from "@utils/discord";
|
||||
import { getCurrentGuild } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
||||
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
||||
|
||||
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||
|
||||
|
@ -49,7 +49,7 @@ export default definePlugin({
|
|||
const guild = getCurrentGuild();
|
||||
if (!guild) return;
|
||||
|
||||
const role = getGuildRoles(guild.id)[id];
|
||||
const role = GuildStore.getRole(guild.id, id);
|
||||
if (!role) return;
|
||||
|
||||
if (role.colorString) {
|
||||
|
|
9
src/plugins/betterSettings/README.md
Normal file
9
src/plugins/betterSettings/README.md
Normal 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)
|
||||
|
177
src/plugins/betterSettings/index.tsx
Normal file
177
src/plugins/betterSettings/index.tsx
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -43,7 +43,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "DefaultCustomizationSections",
|
||||
replacement: {
|
||||
match: /(?<={user:\i},"decoration"\),)/,
|
||||
match: /(?<=USER_SETTINGS_AVATAR_DECORATION},"decoration"\),)/,
|
||||
replace: "$self.DecorSection(),"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -185,7 +185,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
|
|||
|
||||
export default definePlugin({
|
||||
name: "FakeNitro",
|
||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(`
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { RendererSettings } from "@main/settings";
|
||||
import { app } from "electron";
|
||||
import { getSettings } from "main/ipcMain";
|
||||
|
||||
app.on("browser-window-created", (_, win) => {
|
||||
win.webContents.on("frame-created", (_, { frame }) => {
|
||||
frame.once("dom-ready", () => {
|
||||
if (frame.url.startsWith("https://www.youtube.com/")) {
|
||||
const settings = getSettings().plugins?.FixYoutubeEmbeds;
|
||||
const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds;
|
||||
if (!settings?.enabled) return;
|
||||
|
||||
frame.executeJavaScript(`
|
||||
|
|
|
@ -20,8 +20,8 @@ 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 { Menu, React, ReactDOM } from "@webpack/common";
|
||||
import type { Root } from "react-dom/client";
|
||||
|
|
|
@ -27,7 +27,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
|||
|
||||
const { groups } = useStateFromStores(
|
||||
[ChannelMemberStore],
|
||||
() => ChannelMemberStore.getProps(guildId, currentChannel.id)
|
||||
() => ChannelMemberStore.getProps(guildId, currentChannel?.id)
|
||||
);
|
||||
|
||||
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {
|
||||
|
|
|
@ -255,7 +255,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
|
|||
delete msg.embeds;
|
||||
delete msg.interaction;
|
||||
|
||||
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
|
||||
messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)
|
||||
.then(m => m && FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_UPDATE",
|
||||
message: msg
|
||||
|
|
|
@ -137,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",
|
||||
|
@ -197,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 ||
|
||||
|
@ -206,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);
|
||||
},
|
||||
|
||||
|
@ -241,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" +
|
||||
|
|
54
src/plugins/overrideForumDefaults/index.tsx
Normal file
54
src/plugins/overrideForumDefaults/index.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
defaultLayout: {
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "List", value: 1, default: true },
|
||||
{ label: "Gallery", value: 2 }
|
||||
],
|
||||
description: "Which layout to use as default"
|
||||
},
|
||||
defaultSortOrder: {
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Recently Active", value: 0, default: true },
|
||||
{ label: "Date Posted", value: 1 }
|
||||
],
|
||||
description: "Which sort order to use as default"
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "OverrideForumDefaults",
|
||||
description: "Allows you to override default forum layout/sort order. you can still change it on a per-channel basis",
|
||||
authors: [Devs.Inbestigator],
|
||||
patches: [
|
||||
{
|
||||
find: "getDefaultLayout(){",
|
||||
replacement: [
|
||||
{
|
||||
match: /getDefaultLayout\(\){/,
|
||||
replace: "$&return $self.getLayout();"
|
||||
},
|
||||
{
|
||||
match: /getDefaultSortOrder\(\){/,
|
||||
replace: "$&return $self.getSortOrder();"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
getLayout: () => settings.store.defaultLayout,
|
||||
getSortOrder: () => settings.store.defaultSortOrder,
|
||||
|
||||
settings
|
||||
});
|
|
@ -19,9 +19,9 @@
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||
import { getGuildRoles, getUniqueUsername } from "@utils/discord";
|
||||
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,7 +78,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
|||
const [selectedItemIndex, selectItem] = useState(0);
|
||||
const selectedItem = permissions[selectedItemIndex];
|
||||
|
||||
const roles = getGuildRoles(guild.id);
|
||||
const roles = GuildStore.getRoles(guild.id);
|
||||
|
||||
return (
|
||||
<ModalRoot
|
||||
|
@ -203,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 = getGuildRoles(guild.id)[roleId];
|
||||
const role = GuildStore.getRole(guild.id, roleId);
|
||||
if (!role) return;
|
||||
|
||||
onClose();
|
||||
|
|
|
@ -21,7 +21,6 @@ import "./styles.css";
|
|||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getGuildRoles } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore, GuildMemberStore, GuildStore, Menu, PermissionsBits, UserStore } from "@webpack/common";
|
||||
import type { Guild, GuildMember } from "discord-types/general";
|
||||
|
@ -108,7 +107,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
|
|||
}
|
||||
|
||||
default: {
|
||||
permissions = Object.values(getGuildRoles(guild.id)).map(role => ({
|
||||
permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
|
||||
type: PermissionType.Role,
|
||||
...role
|
||||
}));
|
||||
|
|
|
@ -17,9 +17,8 @@
|
|||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { getGuildRoles } from "@utils/discord";
|
||||
import { wordsToTitle } from "@utils/text";
|
||||
import { i18n, Parser } from "@webpack/common";
|
||||
import { GuildStore, i18n, Parser } from "@webpack/common";
|
||||
import { Guild, GuildMember, Role } from "discord-types/general";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
|
@ -69,7 +68,7 @@ export function getPermissionDescription(permission: string): ReactNode {
|
|||
}
|
||||
|
||||
export function getSortedRoles({ id }: Guild, member: GuildMember) {
|
||||
const roles = getGuildRoles(id);
|
||||
const roles = GuildStore.getRoles(id);
|
||||
|
||||
return [...member.roles, id]
|
||||
.map(id => roles[id])
|
||||
|
@ -88,7 +87,7 @@ export function sortUserRoles(roles: Role[]) {
|
|||
}
|
||||
|
||||
export function sortPermissionOverwrites<T extends { id: string; type: number; }>(overwrites: T[], guildId: string) {
|
||||
const roles = getGuildRoles(guildId);
|
||||
const roles = GuildStore.getRoles(guildId);
|
||||
|
||||
return overwrites.sort((a, b) => {
|
||||
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;
|
||||
|
|
134
src/plugins/pinDms/components/CreateCategoryModal.tsx
Normal file
134
src/plugins/pinDms/components/CreateCategoryModal.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
|
||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Forms, Text, TextInput, Toasts, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { DEFAULT_COLOR, SWATCHES } from "../constants";
|
||||
import { categories, Category, createCategory, getCategory, updateCategory } from "../data";
|
||||
import { forceUpdate } from "../index";
|
||||
|
||||
interface ColorPickerProps {
|
||||
color: number | null;
|
||||
showEyeDropper?: boolean;
|
||||
suggestedColors?: string[];
|
||||
onChange(value: number | null): void;
|
||||
}
|
||||
|
||||
interface ColorPickerWithSwatchesProps {
|
||||
defaultColor: number;
|
||||
colors: number[];
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
onChange(value: number | null): void;
|
||||
renderDefaultButton?: () => React.ReactNode;
|
||||
renderCustomButton?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||
const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>(".presets,", "customColor:");
|
||||
|
||||
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}el\("(.+?)"\).{0,50}"UserSettings"/);
|
||||
|
||||
const cl = classNameFactory("vc-pindms-modal-");
|
||||
|
||||
interface Props {
|
||||
categoryId: string | null;
|
||||
initalChannelId: string | null;
|
||||
modalProps: ModalProps;
|
||||
}
|
||||
|
||||
function useCategory(categoryId: string | null, initalChannelId: string | null) {
|
||||
const [category, setCategory] = useState<Category | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (categoryId)
|
||||
setCategory(getCategory(categoryId)!);
|
||||
else if (initalChannelId)
|
||||
setCategory({
|
||||
id: Toasts.genId(),
|
||||
name: `Pin Category ${categories.length + 1}`,
|
||||
color: 10070709,
|
||||
collapsed: false,
|
||||
channels: [initalChannelId]
|
||||
});
|
||||
}, [categoryId, initalChannelId]);
|
||||
|
||||
return {
|
||||
category,
|
||||
setCategory
|
||||
};
|
||||
}
|
||||
|
||||
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) {
|
||||
const { category, setCategory } = useCategory(categoryId, initalChannelId);
|
||||
|
||||
if (!category) return null;
|
||||
|
||||
const onSave = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (!categoryId)
|
||||
await createCategory(category);
|
||||
else
|
||||
await updateCategory(category);
|
||||
|
||||
forceUpdate();
|
||||
modalProps.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{categoryId ? "Edit" : "New"} Category</Text>
|
||||
</ModalHeader>
|
||||
|
||||
{/* form is here so when you press enter while in the text input it submits */}
|
||||
<form onSubmit={onSave}>
|
||||
<ModalContent className={cl("content")}>
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>Name</Forms.FormTitle>
|
||||
<TextInput
|
||||
value={category.name}
|
||||
onChange={e => setCategory({ ...category, name: e })}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
<Forms.FormDivider />
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>Color</Forms.FormTitle>
|
||||
<ColorPickerWithSwatches
|
||||
key={category.name}
|
||||
defaultColor={DEFAULT_COLOR}
|
||||
colors={SWATCHES}
|
||||
onChange={c => setCategory({ ...category, color: c! })}
|
||||
value={category.color}
|
||||
renderDefaultButton={() => null}
|
||||
renderCustomButton={() => (
|
||||
<ColorPicker
|
||||
color={category.color}
|
||||
onChange={c => setCategory({ ...category, color: c! })}
|
||||
key={category.name}
|
||||
showEyeDropper={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button type="submit" onClick={onSave} disabled={!category.name}>{categoryId ? "Save" : "Create"}</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
|
||||
openModalLazy(async () => {
|
||||
await requireSettingsMenu();
|
||||
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initalChannelId={channelId} />;
|
||||
});
|
||||
|
96
src/plugins/pinDms/components/contextMenu.tsx
Normal file
96
src/plugins/pinDms/components/contextMenu.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { Menu } from "@webpack/common";
|
||||
|
||||
import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
|
||||
import { forceUpdate, settings } from "../index";
|
||||
import { openCategoryModal } from "./CreateCategoryModal";
|
||||
|
||||
function createPinMenuItem(channelId: string) {
|
||||
const pinned = isPinned(channelId);
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id="pin-dm"
|
||||
label="Pin DMs"
|
||||
>
|
||||
|
||||
{!pinned && (
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
id="vc-add-category"
|
||||
label="Add Category"
|
||||
color="brand"
|
||||
action={() => openCategoryModal(null, channelId)}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
{
|
||||
categories.map(category => (
|
||||
<Menu.MenuItem
|
||||
id={`pin-category-${category.name}`}
|
||||
label={category.name}
|
||||
action={() => addChannelToCategory(channelId, category.id).then(forceUpdate)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
{pinned && (
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
id="unpin-dm"
|
||||
label="Unpin DM"
|
||||
color="danger"
|
||||
action={() => removeChannelFromCategory(channelId).then(forceUpdate)}
|
||||
/>
|
||||
|
||||
{
|
||||
!settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, -1) && (
|
||||
<Menu.MenuItem
|
||||
id="move-up"
|
||||
label="Move Up"
|
||||
action={() => moveChannel(channelId, -1).then(forceUpdate)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, 1) && (
|
||||
<Menu.MenuItem
|
||||
id="move-down"
|
||||
label="Move Down"
|
||||
action={() => moveChannel(channelId, 1).then(forceUpdate)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => {
|
||||
const container = findGroupChildrenByChildId("leave-channel", children);
|
||||
container?.unshift(createPinMenuItem(props.channel.id));
|
||||
};
|
||||
|
||||
const UserContext: NavContextMenuPatchCallback = (children, props) => {
|
||||
const container = findGroupChildrenByChildId("close-dm", children);
|
||||
if (container) {
|
||||
const idx = container.findIndex(c => c?.props?.id === "close-dm");
|
||||
container.splice(idx, 0, createPinMenuItem(props.channel.id));
|
||||
}
|
||||
};
|
||||
|
||||
export const contextMenus = {
|
||||
"gdm-context": GroupDMContext,
|
||||
"user-context": UserContext
|
||||
};
|
32
src/plugins/pinDms/constants.ts
Normal file
32
src/plugins/pinDms/constants.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export const DEFAULT_CHUNK_SIZE = 256;
|
||||
export const DEFAULT_COLOR = 10070709;
|
||||
|
||||
export const SWATCHES = [
|
||||
1752220,
|
||||
3066993,
|
||||
3447003,
|
||||
10181046,
|
||||
15277667,
|
||||
15844367,
|
||||
15105570,
|
||||
15158332,
|
||||
9807270,
|
||||
6323595,
|
||||
|
||||
1146986,
|
||||
2067276,
|
||||
2123412,
|
||||
7419530,
|
||||
11342935,
|
||||
12745742,
|
||||
11027200,
|
||||
10038562,
|
||||
9936031,
|
||||
5533306
|
||||
];
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { Menu } from "@webpack/common";
|
||||
|
||||
import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings";
|
||||
|
||||
function PinMenuItem(channelId: string) {
|
||||
const pinned = isPinned(channelId);
|
||||
const canMove = pinned && settings.store.pinOrder === PinOrder.Custom;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
id="pin-dm"
|
||||
label={pinned ? "Unpin DM" : "Pin DM"}
|
||||
action={() => togglePin(channelId)}
|
||||
/>
|
||||
{canMove && snapshotArray[0] !== channelId && (
|
||||
<Menu.MenuItem
|
||||
id="move-pin-up"
|
||||
label="Move Pin Up"
|
||||
action={() => movePin(channelId, -1)}
|
||||
/>
|
||||
)}
|
||||
{canMove && snapshotArray[snapshotArray.length - 1] !== channelId && (
|
||||
<Menu.MenuItem
|
||||
id="move-pin-down"
|
||||
label="Move Pin Down"
|
||||
action={() => movePin(channelId, +1)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => {
|
||||
const container = findGroupChildrenByChildId("leave-channel", children);
|
||||
if (container)
|
||||
container.unshift(PinMenuItem(props.channel.id));
|
||||
};
|
||||
|
||||
const UserContext: NavContextMenuPatchCallback = (children, props) => {
|
||||
const container = findGroupChildrenByChildId("close-dm", children);
|
||||
if (container) {
|
||||
const idx = container.findIndex(c => c?.props?.id === "close-dm");
|
||||
container.splice(idx, 0, PinMenuItem(props.channel.id));
|
||||
}
|
||||
};
|
||||
|
||||
export const contextMenus = {
|
||||
"gdm-context": GroupDMContext,
|
||||
"user-context": UserContext
|
||||
};
|
214
src/plugins/pinDms/data.ts
Normal file
214
src/plugins/pinDms/data.ts
Normal file
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
import { DEFAULT_COLOR } from "./constants";
|
||||
import { forceUpdate } from "./index";
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
color: number;
|
||||
channels: string[];
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
const CATEGORY_BASE_KEY = "PinDMsCategories-";
|
||||
const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs";
|
||||
const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories";
|
||||
const OLD_CATEGORY_KEY = "BetterPinDMsCategories-";
|
||||
|
||||
|
||||
export let categories: Category[] = [];
|
||||
|
||||
export async function saveCats(cats: Category[]) {
|
||||
const { id } = UserStore.getCurrentUser();
|
||||
await DataStore.set(CATEGORY_BASE_KEY + id, cats);
|
||||
}
|
||||
|
||||
export async function init() {
|
||||
const id = UserStore.getCurrentUser()?.id;
|
||||
await initCategories(id);
|
||||
await migrateData(id);
|
||||
forceUpdate();
|
||||
}
|
||||
|
||||
export async function initCategories(userId: string) {
|
||||
categories = await DataStore.get<Category[]>(CATEGORY_BASE_KEY + userId) ?? [];
|
||||
}
|
||||
|
||||
export function getCategory(id: string) {
|
||||
return categories.find(c => c.id === id);
|
||||
}
|
||||
|
||||
export async function createCategory(category: Category) {
|
||||
categories.push(category);
|
||||
await saveCats(categories);
|
||||
}
|
||||
|
||||
export async function updateCategory(category: Category) {
|
||||
const index = categories.findIndex(c => c.id === category.id);
|
||||
if (index === -1) return;
|
||||
|
||||
categories[index] = category;
|
||||
await saveCats(categories);
|
||||
}
|
||||
|
||||
export async function addChannelToCategory(channelId: string, categoryId: string) {
|
||||
const category = categories.find(c => c.id === categoryId);
|
||||
if (!category) return;
|
||||
|
||||
if (category.channels.includes(channelId)) return;
|
||||
|
||||
category.channels.push(channelId);
|
||||
await saveCats(categories);
|
||||
|
||||
}
|
||||
|
||||
export async function removeChannelFromCategory(channelId: string) {
|
||||
const category = categories.find(c => c.channels.includes(channelId));
|
||||
if (!category) return;
|
||||
|
||||
category.channels = category.channels.filter(c => c !== channelId);
|
||||
await saveCats(categories);
|
||||
}
|
||||
|
||||
export async function removeCategory(categoryId: string) {
|
||||
const catagory = categories.find(c => c.id === categoryId);
|
||||
if (!catagory) return;
|
||||
|
||||
// catagory?.channels.forEach(c => removeChannelFromCategory(c));
|
||||
categories = categories.filter(c => c.id !== categoryId);
|
||||
await saveCats(categories);
|
||||
}
|
||||
|
||||
export async function collapseCategory(id: string, value = true) {
|
||||
const category = categories.find(c => c.id === id);
|
||||
if (!category) return;
|
||||
|
||||
category.collapsed = value;
|
||||
await saveCats(categories);
|
||||
}
|
||||
|
||||
// utils
|
||||
export function isPinned(id: string) {
|
||||
return categories.some(c => c.channels.includes(id));
|
||||
}
|
||||
|
||||
export function categoryLen() {
|
||||
return categories.length;
|
||||
}
|
||||
|
||||
export function getAllUncollapsedChannels() {
|
||||
return categories.filter(c => !c.collapsed).map(c => c.channels).flat();
|
||||
}
|
||||
|
||||
export function getSections() {
|
||||
return categories.reduce((acc, category) => {
|
||||
acc.push(category.channels.length === 0 ? 1 : category.channels.length);
|
||||
return acc;
|
||||
}, [] as number[]);
|
||||
}
|
||||
|
||||
// move categories
|
||||
export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {
|
||||
const a = array[index];
|
||||
const b = array[index + direction];
|
||||
|
||||
return a && b;
|
||||
};
|
||||
|
||||
export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {
|
||||
const index = categories.findIndex(m => m.id === id);
|
||||
return canMoveArrayInDirection(categories, index, direction);
|
||||
};
|
||||
|
||||
export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);
|
||||
|
||||
export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {
|
||||
const category = categories.find(c => c.channels.includes(channelId));
|
||||
if (!category) return false;
|
||||
|
||||
const index = category.channels.indexOf(channelId);
|
||||
return canMoveArrayInDirection(category.channels, index, direction);
|
||||
};
|
||||
|
||||
|
||||
function swapElementsInArray(array: any[], index1: number, index2: number) {
|
||||
if (!array[index1] || !array[index2]) return;
|
||||
[array[index1], array[index2]] = [array[index2], array[index1]];
|
||||
}
|
||||
|
||||
// stolen from PinDMs
|
||||
export async function moveCategory(id: string, direction: -1 | 1) {
|
||||
const a = categories.findIndex(m => m.id === id);
|
||||
const b = a + direction;
|
||||
|
||||
swapElementsInArray(categories, a, b);
|
||||
|
||||
await saveCats(categories);
|
||||
}
|
||||
|
||||
export async function moveChannel(channelId: string, direction: -1 | 1) {
|
||||
const category = categories.find(c => c.channels.includes(channelId));
|
||||
if (!category) return;
|
||||
|
||||
const a = category.channels.indexOf(channelId);
|
||||
const b = a + direction;
|
||||
|
||||
swapElementsInArray(category.channels, a, b);
|
||||
|
||||
await saveCats(categories);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// migrate data
|
||||
const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
|
||||
|
||||
async function migratePinDMs() {
|
||||
if (categories.some(m => m.id === "oldPins")) {
|
||||
return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
|
||||
}
|
||||
|
||||
const pindmspins = getPinDMsPins();
|
||||
|
||||
// we dont want duplicate pins
|
||||
const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m)));
|
||||
if (difference?.length) {
|
||||
categories.push({
|
||||
id: "oldPins",
|
||||
name: "Pins",
|
||||
color: DEFAULT_COLOR,
|
||||
channels: difference
|
||||
});
|
||||
}
|
||||
|
||||
await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
|
||||
}
|
||||
|
||||
async function migrateOldCategories(userId: string) {
|
||||
const oldCats = await DataStore.get<Category[]>(OLD_CATEGORY_KEY + userId);
|
||||
// dont want to migrate if the user has already has categories.
|
||||
if (categories.length === 0 && oldCats?.length) {
|
||||
categories.push(...(oldCats.filter(m => m.id !== "oldPins")));
|
||||
}
|
||||
await DataStore.set(CATEGORY_MIGRATED_KEY, true);
|
||||
}
|
||||
|
||||
export async function migrateData(userId: string) {
|
||||
const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY);
|
||||
if (m1 && m2) return;
|
||||
|
||||
// want to migrate the old categories first and then slove any conflicts with the PinDMs pins
|
||||
if (!m1) await migrateOldCategories(userId);
|
||||
if (!m2) await migratePinDMs();
|
||||
|
||||
await saveCats(categories);
|
||||
}
|
|
@ -1,116 +1,131 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
|
||||
import { contextMenus } from "./contextMenus";
|
||||
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings";
|
||||
import { contextMenus } from "./components/contextMenu";
|
||||
import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal";
|
||||
import { DEFAULT_CHUNK_SIZE } from "./constants";
|
||||
import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data";
|
||||
|
||||
interface ChannelComponentProps {
|
||||
children: React.ReactNode,
|
||||
channel: Channel,
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
|
||||
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
|
||||
|
||||
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
||||
|
||||
export let instance: any;
|
||||
export const forceUpdate = () => instance?.props?._forceUpdate?.();
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
sortDmsByNewestMessage: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Sort DMs by newest message",
|
||||
default: false,
|
||||
onChange: () => forceUpdate()
|
||||
},
|
||||
|
||||
dmSectioncollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Collapse DM sections",
|
||||
default: false,
|
||||
onChange: () => forceUpdate()
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "PinDMs",
|
||||
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
|
||||
authors: [Devs.Ven, Devs.Strencher],
|
||||
|
||||
authors: [Devs.Ven, Devs.Aria],
|
||||
settings,
|
||||
contextMenus,
|
||||
|
||||
usePinCount(channelIds: string[]) {
|
||||
const pinnedDms = usePinnedDms();
|
||||
// See comment on 2nd patch for reasoning
|
||||
return channelIds.length ? [pinnedDms.size] : [];
|
||||
},
|
||||
|
||||
getChannel(channels: Record<string, Channel>, idx: number) {
|
||||
return channels[getPinAt(idx)];
|
||||
},
|
||||
|
||||
isPinned,
|
||||
getSnapshot: sortedSnapshot,
|
||||
|
||||
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
|
||||
if (!isPinned(channelId))
|
||||
return (
|
||||
(rowHeight + padding) * 2 // header
|
||||
+ rowHeight * snapshotArray.length // pins
|
||||
+ originalOffset // original pin offset minus pins
|
||||
);
|
||||
|
||||
return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding;
|
||||
},
|
||||
|
||||
patches: [
|
||||
// Patch DM list
|
||||
{
|
||||
find: ".privateChannelsHeaderContainer,",
|
||||
replacement: [
|
||||
// Init
|
||||
{
|
||||
// filter Discord's privateChannelIds list to remove pins, and pass
|
||||
// pinCount as prop. This needs to be here so that the entire DM list receives
|
||||
// updates on pin/unpin
|
||||
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i),/,
|
||||
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1),"
|
||||
match: /(?<=componentDidMount\(\){).{1,100}scrollToChannel/,
|
||||
replace: "$self._instance = this;$&"
|
||||
},
|
||||
{
|
||||
// sections is an array of numbers, where each element is a section and
|
||||
// the number is the amount of rows. Add our pinCount in second place
|
||||
// - Section 1: buttons for pages like Friends & Library
|
||||
// - Section 2: our pinned dms
|
||||
// - Section 3: the normal dm list
|
||||
match: /(?<=renderRow:this\.renderRow,)sections:\[\i,/,
|
||||
// For some reason, adding our sections when no private channels are ready yet
|
||||
// makes DMs infinitely load. Thus usePinCount returns either a single element
|
||||
// array with the count, or an empty array. Due to spreading, only in the former
|
||||
// case will an element be added to the outer array
|
||||
// Thanks for the fix, Strencher!
|
||||
replace: "$&...this.props.pinCount??[],"
|
||||
// Filter out pinned channels from the private channel list
|
||||
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/,
|
||||
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))"
|
||||
},
|
||||
{
|
||||
// Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages"
|
||||
// lookbehind is used to lookup parameter name. We could use arguments[0], but
|
||||
// if children ever is wrapped in an iife, it will break
|
||||
match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=(\i)=>{.+?)/,
|
||||
replace: "children:$2.section===1?'Pinned DMs':$1"
|
||||
// Insert the pinned channels to sections
|
||||
match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/,
|
||||
replace: "...$self.makeProps(this,{$&})"
|
||||
},
|
||||
|
||||
// Rendering
|
||||
{
|
||||
match: /this\.renderDM=\(.+?(\i\.default),{channel.+?this.renderRow=(\i)=>{/,
|
||||
replace: "$&if($self.isChannelIndex($2.section, $2.row))return $self.renderChannel($2.section,$2.row,$1);"
|
||||
},
|
||||
{
|
||||
// Patch channel lookup inside renderDM
|
||||
// channel=channels[channelIds[row]];
|
||||
match: /(?<=renderDM=\((\i),(\i)\)=>{.*?this.state,\i=\i\[\i\],\i=)((\i)\[\i\]);/,
|
||||
// section 1 is us, manually get our own channel
|
||||
// section === 1 ? getChannel(channels, row) : channels[channelIds[row]];
|
||||
replace: "$1===1?$self.getChannel($4,$2):$3;"
|
||||
match: /this\.renderSection=(\i)=>{/,
|
||||
replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);"
|
||||
},
|
||||
{
|
||||
// Fix getRowHeight's check for whether this is the DMs section
|
||||
// DMS (inlined) === section
|
||||
match: /(?<=getRowHeight=\(.{2,50}?)1===\i/,
|
||||
// DMS (inlined) === section - 1
|
||||
replace: "$&-1"
|
||||
match: /(?<=span",{)className:\i\.headerText,/,
|
||||
replace: "...$self.makeSpanProps(),$&"
|
||||
},
|
||||
|
||||
// Fix Row Height
|
||||
{
|
||||
match: /(?<=this\.getRowHeight=.{1,100}return 1===)\i/,
|
||||
replace: "($&-$self.categoryLen())"
|
||||
},
|
||||
{
|
||||
match: /this.getRowHeight=\((\i),(\i)\)=>{/,
|
||||
replace: "$&if($self.isChannelHidden($1,$2))return 0;"
|
||||
},
|
||||
|
||||
// Fix ScrollTo
|
||||
{
|
||||
// Override scrollToChannel to properly account for pinned channels
|
||||
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
|
||||
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
|
||||
}
|
||||
},
|
||||
{
|
||||
match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/,
|
||||
replace: "[...$&,...$self.getAllUncollapsedChannels()]"
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
// forceUpdate moment
|
||||
// https://regex101.com/r/kDN9fO/1
|
||||
{
|
||||
find: ".FRIENDS},\"friends\"",
|
||||
replacement: {
|
||||
match: /(?<=\i=\i=>{).{1,100}premiumTabSelected.{1,800}showDMHeader:.+?,/,
|
||||
replace: "let forceUpdate = Vencord.Util.useForceUpdater();$&_forceUpdate:forceUpdate,"
|
||||
}
|
||||
},
|
||||
|
||||
// Fix Alt Up/Down navigation
|
||||
{
|
||||
find: ".Routes.APPLICATION_STORE&&",
|
||||
|
@ -118,16 +133,227 @@ export default definePlugin({
|
|||
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
|
||||
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
|
||||
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
|
||||
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||
}
|
||||
},
|
||||
|
||||
// fix alt+shift+up/down
|
||||
{
|
||||
find: ".getFlattenedGuildIds()],",
|
||||
replacement: {
|
||||
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
|
||||
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||
}
|
||||
},
|
||||
]
|
||||
],
|
||||
sections: null as number[] | null,
|
||||
|
||||
set _instance(i: any) {
|
||||
this.instance = i;
|
||||
instance = i;
|
||||
},
|
||||
|
||||
startAt: StartAt.WebpackReady,
|
||||
start: init,
|
||||
flux: {
|
||||
CONNECTION_OPEN: init,
|
||||
},
|
||||
|
||||
isPinned,
|
||||
categoryLen,
|
||||
getSections,
|
||||
getAllUncollapsedChannels,
|
||||
requireSettingsMenu,
|
||||
makeProps(instance, { sections }: { sections: number[]; }) {
|
||||
this.sections = sections;
|
||||
|
||||
this.sections.splice(1, 0, ...this.getPinCount(instance.props.privateChannelIds || []));
|
||||
|
||||
if (this.instance?.props?.privateChannelIds?.length === 0) {
|
||||
this.sections[this.sections.length - 1] = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
sections: this.sections,
|
||||
chunkSize: this.getChunkSize(),
|
||||
};
|
||||
},
|
||||
|
||||
makeSpanProps() {
|
||||
return {
|
||||
onClick: () => this.collapseDMList(),
|
||||
role: "button",
|
||||
style: { cursor: "pointer" }
|
||||
};
|
||||
},
|
||||
|
||||
getChunkSize() {
|
||||
// the chunk size is the amount of rows (measured in pixels) that are rendered at once (probably)
|
||||
// the higher the chunk size, the more rows are rendered at once
|
||||
// also if the chunk size is 0 it will render everything at once
|
||||
|
||||
const sections = this.getSections();
|
||||
const sectionHeaderSizePx = sections.length * 40;
|
||||
// (header heights + DM heights + DEFAULT_CHUNK_SIZE) * 1.5
|
||||
// we multiply everything by 1.5 so it only gets unmounted after the entire list is off screen
|
||||
return (sectionHeaderSizePx + sections.reduce((acc, v) => acc += v + 44, 0) + DEFAULT_CHUNK_SIZE) * 1.5;
|
||||
},
|
||||
|
||||
getPinCount(channelIds: string[]) {
|
||||
return channelIds.length ? this.getSections() : [];
|
||||
},
|
||||
|
||||
isCategoryIndex(sectionIndex: number) {
|
||||
return this.sections && sectionIndex > 0 && sectionIndex < this.sections.length - 1;
|
||||
},
|
||||
|
||||
isChannelIndex(sectionIndex: number, channelIndex: number) {
|
||||
if (settings.store.dmSectioncollapsed && sectionIndex !== 0)
|
||||
return true;
|
||||
const cat = categories[sectionIndex - 1];
|
||||
return this.isCategoryIndex(sectionIndex) && (cat.channels.length === 0 || cat?.channels[channelIndex]);
|
||||
},
|
||||
|
||||
isDMSectioncollapsed() {
|
||||
return settings.store.dmSectioncollapsed;
|
||||
},
|
||||
|
||||
collapseDMList() {
|
||||
// console.log("HI");
|
||||
settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed;
|
||||
forceUpdate();
|
||||
},
|
||||
|
||||
isChannelHidden(categoryIndex: number, channelIndex: number) {
|
||||
if (categoryIndex === 0) return false;
|
||||
|
||||
if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex)
|
||||
return true;
|
||||
|
||||
if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;
|
||||
|
||||
const category = categories[categoryIndex - 1];
|
||||
if (!category) return false;
|
||||
|
||||
return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex];
|
||||
},
|
||||
|
||||
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
|
||||
if (!isPinned(channelId))
|
||||
return (
|
||||
(rowHeight + padding) * 2 // header
|
||||
+ rowHeight * this.getAllUncollapsedChannels().length // pins
|
||||
+ originalOffset // original pin offset minus pins
|
||||
);
|
||||
|
||||
return rowHeight * (this.getAllUncollapsedChannels().indexOf(channelId) + preRenderedChildren) + padding;
|
||||
},
|
||||
|
||||
renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => {
|
||||
const category = categories[section - 1];
|
||||
|
||||
if (!category) return null;
|
||||
|
||||
return (
|
||||
<h2
|
||||
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
|
||||
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
|
||||
onClick={async () => {
|
||||
await collapseCategory(category.id, !category.collapsed);
|
||||
forceUpdate();
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
ContextMenuApi.openContextMenu(e, () => (
|
||||
<Menu.Menu
|
||||
navId="vc-pindms-header-menu"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
color="danger"
|
||||
aria-label="Pin DMs Category Menu"
|
||||
>
|
||||
<Menu.MenuItem
|
||||
id="vc-pindms-edit-category"
|
||||
label="Edit Category"
|
||||
action={() => openCategoryModal(category.id, null)}
|
||||
/>
|
||||
|
||||
{
|
||||
canMoveCategory(category.id) && (
|
||||
<>
|
||||
{
|
||||
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
|
||||
id="vc-pindms-move-category-up"
|
||||
label="Move Up"
|
||||
action={() => moveCategory(category.id, -1).then(() => forceUpdate())}
|
||||
/>
|
||||
}
|
||||
{
|
||||
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
|
||||
id="vc-pindms-move-category-down"
|
||||
label="Move Down"
|
||||
action={() => moveCategory(category.id, 1).then(() => forceUpdate())}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
id="vc-pindms-delete-category"
|
||||
color="danger"
|
||||
label="Delete Category"
|
||||
action={() => removeCategory(category.id).then(() => forceUpdate())}
|
||||
/>
|
||||
|
||||
|
||||
</Menu.Menu>
|
||||
));
|
||||
}}
|
||||
>
|
||||
<span className={headerClasses.headerText}>
|
||||
{category?.name ?? "uh oh"}
|
||||
</span>
|
||||
<svg className="vc-pindms-collapse-icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
|
||||
</svg>
|
||||
</h2>
|
||||
);
|
||||
}),
|
||||
|
||||
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
|
||||
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
|
||||
|
||||
if (!channel || !category) return null;
|
||||
if (this.isChannelHidden(sectionIndex, index)) return null;
|
||||
|
||||
return (
|
||||
<ChannelComponent
|
||||
channel={channel}
|
||||
selected={this.instance.props.selectedChannelId === channel.id}
|
||||
>
|
||||
{channel.id}
|
||||
</ChannelComponent>
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
||||
const category = categories[sectionIndex - 1];
|
||||
if (!category) return { channel: null, category: null };
|
||||
|
||||
const channelId = this.getCategoryChannels(category)[index];
|
||||
|
||||
return { channel: channels[channelId], category };
|
||||
},
|
||||
|
||||
getCategoryChannels(category: Category) {
|
||||
if (category.channels.length === 0) return [];
|
||||
|
||||
if (settings.store.sortDmsByNewestMessage) {
|
||||
return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category.channels.includes(c));
|
||||
}
|
||||
|
||||
return category?.channels ?? [];
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings, Settings, useSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
|
||||
export const enum PinOrder {
|
||||
LastMessage,
|
||||
Custom
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
pinOrder: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Which order should pinned DMs be displayed in?",
|
||||
options: [
|
||||
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
|
||||
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore");
|
||||
|
||||
export let snapshotArray: string[];
|
||||
let snapshot: Set<string> | undefined;
|
||||
|
||||
const getArray = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
|
||||
const save = (pins: string[]) => {
|
||||
snapshot = void 0;
|
||||
Settings.plugins.PinDMs.pinnedDMs = pins.join(",");
|
||||
};
|
||||
const takeSnapshot = () => {
|
||||
snapshotArray = getArray() ?? [];
|
||||
return snapshot = new Set<string>(snapshotArray);
|
||||
};
|
||||
const requireSnapshot = () => snapshot ?? takeSnapshot();
|
||||
|
||||
export function usePinnedDms() {
|
||||
useSettings(["plugins.PinDMs.pinnedDMs"]);
|
||||
|
||||
return requireSnapshot();
|
||||
}
|
||||
|
||||
export function isPinned(id: string) {
|
||||
return requireSnapshot().has(id);
|
||||
}
|
||||
|
||||
export function togglePin(id: string) {
|
||||
const snapshot = requireSnapshot();
|
||||
if (!snapshot.delete(id)) {
|
||||
snapshot.add(id);
|
||||
}
|
||||
|
||||
save([...snapshot]);
|
||||
}
|
||||
|
||||
export function sortedSnapshot() {
|
||||
requireSnapshot();
|
||||
if (settings.store.pinOrder === PinOrder.LastMessage)
|
||||
return PrivateChannelSortStore.getPrivateChannelIds().filter(isPinned);
|
||||
|
||||
return snapshotArray;
|
||||
}
|
||||
|
||||
export function getPinAt(idx: number) {
|
||||
return sortedSnapshot()[idx];
|
||||
}
|
||||
|
||||
export function movePin(id: string, direction: -1 | 1) {
|
||||
const pins = getArray()!;
|
||||
const a = pins.indexOf(id);
|
||||
const b = a + direction;
|
||||
|
||||
[pins[a], pins[b]] = [pins[b], pins[a]];
|
||||
|
||||
save(pins);
|
||||
}
|
37
src/plugins/pinDms/styles.css
Normal file
37
src/plugins/pinDms/styles.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
.vc-pindms-section-container {
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: .02em;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
flex: 1 1 auto;
|
||||
color: var(--channels-default);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-pindms-modal-content {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.vc-pindms-modal-content [class^="defaultContainer"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vc-pindms-collapse-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--interactive-normal);
|
||||
transform: rotate(90deg)
|
||||
}
|
||||
|
||||
.vc-pindms-collapsed .vc-pindms-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ import ReviewComponent from "./ReviewComponent";
|
|||
const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms");
|
||||
const { ChatInputTypes } = findByPropsLazy("ChatInputTypes");
|
||||
|
||||
const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA");
|
||||
const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA", "input");
|
||||
const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer");
|
||||
|
||||
interface UserProps {
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getGuildRoles } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore, GuildMemberStore } from "@webpack/common";
|
||||
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
chatMentions: {
|
||||
|
@ -115,7 +114,7 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
roleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => {
|
||||
const role = getGuildRoles(guildId)[id];
|
||||
const role = GuildStore.getRole(guildId, id);
|
||||
|
||||
return (
|
||||
<span style={{
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
import "./styles.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { getGuildRoles, openImageModal, openUserProfile } from "@utils/discord";
|
||||
import { openImageModal, openUserProfile } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { findByPropsLazy, findExportedComponentLazy } from "@webpack";
|
||||
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
||||
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
||||
import { Guild, User } from "discord-types/general";
|
||||
|
||||
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
||||
|
@ -172,7 +172,7 @@ function ServerInfoTab({ guild }: GuildProps) {
|
|||
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
|
||||
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
|
||||
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
|
||||
"Roles": Object.keys(getGuildRoles(guild.id)).length - 1, // - @everyone
|
||||
"Roles": Object.keys(GuildStore.getRoles(guild.id)).length - 1, // - @everyone
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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(",
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -174,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,'
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getGuildRoles } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType, PluginNative } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
|
@ -197,7 +196,7 @@ export default definePlugin({
|
|||
|
||||
if (message.mention_roles.length > 0) {
|
||||
for (const roleId of message.mention_roles) {
|
||||
const role = getGuildRoles(channel.guild_id)[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>`);
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { contextBridge, webFrame } from "electron";
|
||||
import { readFileSync, watch } from "fs";
|
||||
import { join } from "path";
|
||||
|
|
182
src/shared/SettingsStore.ts
Normal file
182
src/shared/SettingsStore.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { LiteralUnion } from "type-fest";
|
||||
|
||||
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
||||
type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}`
|
||||
? Pre extends keyof T
|
||||
? ResolvePropDeep<T[Pre], Suf>
|
||||
: any
|
||||
: P extends keyof T
|
||||
? T[P]
|
||||
: any;
|
||||
|
||||
interface SettingsStoreOptions {
|
||||
readOnly?: boolean;
|
||||
getDefaultValue?: (data: {
|
||||
target: any;
|
||||
key: string;
|
||||
root: any;
|
||||
path: string;
|
||||
}) => any;
|
||||
}
|
||||
|
||||
// merges the SettingsStoreOptions type into the class
|
||||
export interface SettingsStore<T extends object> extends SettingsStoreOptions { }
|
||||
|
||||
/**
|
||||
* The SettingsStore allows you to easily create a mutable store that
|
||||
* has support for global and path-based change listeners.
|
||||
*/
|
||||
export class SettingsStore<T extends object> {
|
||||
private pathListeners = new Map<string, Set<(newData: any) => void>>();
|
||||
private globalListeners = new Set<(newData: T, path: string) => void>();
|
||||
|
||||
/**
|
||||
* The store object. Making changes to this object will trigger the applicable change listeners
|
||||
*/
|
||||
public declare store: T;
|
||||
/**
|
||||
* The plain data. Changes to this object will not trigger any change listeners
|
||||
*/
|
||||
public declare plain: T;
|
||||
|
||||
public constructor(plain: T, options: SettingsStoreOptions = {}) {
|
||||
this.plain = plain;
|
||||
this.store = this.makeProxy(plain);
|
||||
Object.assign(this, options);
|
||||
}
|
||||
|
||||
private makeProxy(object: any, root: T = object, path: string = "") {
|
||||
const self = this;
|
||||
|
||||
return new Proxy(object, {
|
||||
get(target, key: string) {
|
||||
let v = target[key];
|
||||
|
||||
if (!(key in target) && self.getDefaultValue) {
|
||||
v = self.getDefaultValue({
|
||||
target,
|
||||
key,
|
||||
root,
|
||||
path
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof v === "object" && v !== null && !Array.isArray(v))
|
||||
return self.makeProxy(v, root, `${path}${path && "."}${key}`);
|
||||
|
||||
return v;
|
||||
},
|
||||
set(target, key: string, value) {
|
||||
if (target[key] === value) return true;
|
||||
|
||||
Reflect.set(target, key, value);
|
||||
const setPath = `${path}${path && "."}${key}`;
|
||||
|
||||
self.globalListeners.forEach(cb => cb(value, setPath));
|
||||
self.pathListeners.get(setPath)?.forEach(cb => cb(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data of the store.
|
||||
* This will update this.store and this.plain (and old references to them will be stale! Avoid storing them in variables)
|
||||
*
|
||||
* Additionally, all global listeners (and those for pathToNotify, if specified) will be called with the new data
|
||||
* @param value New data
|
||||
* @param pathToNotify Optional path to notify instead of globally. Used to transfer path via ipc
|
||||
*/
|
||||
public setData(value: T, pathToNotify?: string) {
|
||||
if (this.readOnly) throw new Error("SettingsStore is read-only");
|
||||
|
||||
this.plain = value;
|
||||
this.store = this.makeProxy(value);
|
||||
|
||||
if (pathToNotify) {
|
||||
let v = value;
|
||||
|
||||
const path = pathToNotify.split(".");
|
||||
for (const p of path) {
|
||||
if (!v) {
|
||||
console.warn(
|
||||
`Settings#setData: Path ${pathToNotify} does not exist in new data. Not dispatching update`
|
||||
);
|
||||
return;
|
||||
}
|
||||
v = v[p];
|
||||
}
|
||||
|
||||
this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v));
|
||||
}
|
||||
|
||||
this.markAsChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a global change listener, that will fire whenever any setting is changed
|
||||
*
|
||||
* @param data The new data. This is either the new value set on the path, or the new root object if it was changed
|
||||
* @param path The path of the setting that was changed. Empty string if the root object was changed
|
||||
*/
|
||||
public addGlobalChangeListener(cb: (data: any, path: string) => void) {
|
||||
this.globalListeners.add(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a scoped change listener that will fire whenever a setting matching the specified path is changed.
|
||||
*
|
||||
* For example if path is `"foo.bar"`, the listener will fire on
|
||||
* ```js
|
||||
* Setting.store.foo.bar = "hi"
|
||||
* ```
|
||||
* but not on
|
||||
* ```js
|
||||
* Setting.store.foo.baz = "hi"
|
||||
* ```
|
||||
* @param path
|
||||
* @param cb
|
||||
*/
|
||||
public addChangeListener<P extends LiteralUnion<keyof T, string>>(
|
||||
path: P,
|
||||
cb: (data: ResolvePropDeep<T, P>) => void
|
||||
) {
|
||||
const listeners = this.pathListeners.get(path as string) ?? new Set();
|
||||
listeners.add(cb);
|
||||
this.pathListeners.set(path as string, listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a global listener
|
||||
* @see {@link addGlobalChangeListener}
|
||||
*/
|
||||
public removeGlobalChangeListener(cb: (data: any, path: string) => void) {
|
||||
this.globalListeners.delete(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a scoped listener
|
||||
* @see {@link addChangeListener}
|
||||
*/
|
||||
public removeChangeListener(path: LiteralUnion<keyof T, string>, cb: (data: any) => void) {
|
||||
const listeners = this.pathListeners.get(path as string);
|
||||
if (!listeners) return;
|
||||
|
||||
listeners.delete(cb);
|
||||
if (!listeners.size) this.pathListeners.delete(path as string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call all global change listeners
|
||||
*/
|
||||
public markAsChanged() {
|
||||
this.globalListeners.forEach(cb => cb(this.plain, ""));
|
||||
}
|
||||
}
|
12
src/shared/vencordUserAgent.ts
Normal file
12
src/shared/vencordUserAgent.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
import gitRemote from "~git-remote";
|
||||
|
||||
export { gitHash, gitRemote };
|
||||
|
||||
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`;
|
|
@ -16,17 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
import gitRemote from "~git-remote";
|
||||
|
||||
export {
|
||||
gitHash,
|
||||
gitRemote
|
||||
};
|
||||
|
||||
export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
|
||||
export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
|
||||
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`;
|
||||
export const SUPPORT_CHANNEL_ID = "1026515880080842772";
|
||||
|
||||
export interface Dev {
|
||||
|
@ -58,6 +49,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Cynosphere",
|
||||
id: 150745989836308480n
|
||||
},
|
||||
Trwy: {
|
||||
name: "trey",
|
||||
id: 354427199023218689n
|
||||
},
|
||||
Megu: {
|
||||
name: "Megumin",
|
||||
id: 545581357812678656n
|
||||
|
@ -66,8 +61,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "botato",
|
||||
id: 440990343899643943n
|
||||
},
|
||||
obscurity: {
|
||||
name: "obscurity",
|
||||
fawn: {
|
||||
name: "fawn",
|
||||
id: 336678828233588736n,
|
||||
},
|
||||
rushii: {
|
||||
|
@ -291,10 +286,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "RyanCaoDev",
|
||||
id: 952235800110694471n,
|
||||
},
|
||||
Strencher: {
|
||||
name: "Strencher",
|
||||
id: 415849376598982656n
|
||||
},
|
||||
FieryFlames: {
|
||||
name: "Fiery",
|
||||
id: 890228870559698955n
|
||||
|
@ -426,6 +417,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
Elvyra: {
|
||||
name: "Elvyra",
|
||||
id: 708275751816003615n,
|
||||
},
|
||||
Inbestigator: {
|
||||
name: "Inbestigator",
|
||||
id: 761777382041714690n
|
||||
},
|
||||
newwares: {
|
||||
name: "newwares",
|
||||
id: 421405303951851520n
|
||||
}
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { MessageObject } from "@api/MessageEvents";
|
||||
import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
|
||||
import { Guild, Message, Role, User } from "discord-types/general";
|
||||
import { Guild, Message, User } from "discord-types/general";
|
||||
|
||||
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
|
||||
|
||||
|
@ -185,11 +185,3 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt
|
|||
export function getUniqueUsername(user: User) {
|
||||
return user.discriminator === "0" ? user.username : user.tag;
|
||||
}
|
||||
|
||||
// FIXME: remove this once discord merges the role change into stable
|
||||
export function getGuildRoles(guildId: string): Record<string, Role> {
|
||||
if ("getRoles" in GuildStore)
|
||||
return (GuildStore as any).getRoles(guildId);
|
||||
|
||||
return GuildStore.getGuild(guildId)?.roles ?? {};
|
||||
}
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "../shared/debounce";
|
||||
export * from "../shared/onceDefined";
|
||||
export * from "./ChangeList";
|
||||
export * from "./constants";
|
||||
export * from "./debounce";
|
||||
export * from "./discord";
|
||||
export * from "./guards";
|
||||
export * from "./lazy";
|
||||
|
@ -27,7 +28,6 @@ export * from "./Logger";
|
|||
export * from "./margins";
|
||||
export * from "./misc";
|
||||
export * from "./modal";
|
||||
export * from "./onceDefined";
|
||||
export * from "./onlyOnce";
|
||||
export * from "./patches";
|
||||
export * from "./Queue";
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addSettingsListener, Settings } from "@api/Settings";
|
||||
import { Settings, SettingsStore } from "@api/Settings";
|
||||
|
||||
|
||||
let style: HTMLStyleElement;
|
||||
|
@ -81,10 +81,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
initThemes();
|
||||
|
||||
toggle(Settings.useQuickCss);
|
||||
addSettingsListener("useQuickCss", toggle);
|
||||
SettingsStore.addChangeListener("useQuickCss", toggle);
|
||||
|
||||
addSettingsListener("themeLinks", initThemes);
|
||||
addSettingsListener("enabledThemes", initThemes);
|
||||
SettingsStore.addChangeListener("themeLinks", initThemes);
|
||||
SettingsStore.addChangeListener("enabledThemes", initThemes);
|
||||
|
||||
if (!IS_WEB)
|
||||
VencordNative.quickCss.addThemeChangeListener(initThemes);
|
||||
|
|
|
@ -36,14 +36,14 @@ export async function importSettings(data: string) {
|
|||
|
||||
if ("settings" in parsed && "quickCss" in parsed) {
|
||||
Object.assign(PlainSettings, parsed.settings);
|
||||
await VencordNative.settings.set(JSON.stringify(parsed.settings, null, 4));
|
||||
await VencordNative.settings.set(parsed.settings);
|
||||
await VencordNative.quickCss.set(parsed.quickCss);
|
||||
} else
|
||||
throw new Error("Invalid Settings. Is this even a Vencord Settings file?");
|
||||
}
|
||||
|
||||
export async function exportSettings({ minify }: { minify?: boolean; } = {}) {
|
||||
const settings = JSON.parse(VencordNative.settings.get());
|
||||
const settings = VencordNative.settings.get();
|
||||
const quickCss = await VencordNative.quickCss.get();
|
||||
return JSON.stringify({ settings, quickCss }, null, minify ? undefined : 4);
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ export async function putCloudSettings(manual?: boolean) {
|
|||
|
||||
const { written } = await res.json();
|
||||
PlainSettings.cloud.settingsSyncVersion = written;
|
||||
VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4));
|
||||
VencordNative.settings.set(PlainSettings);
|
||||
|
||||
cloudSettingsLogger.info("Settings uploaded to cloud successfully");
|
||||
|
||||
|
@ -222,7 +222,7 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
|||
|
||||
// sync with server timestamp instead of local one
|
||||
PlainSettings.cloud.settingsSyncVersion = written;
|
||||
VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4));
|
||||
VencordNative.settings.set(PlainSettings);
|
||||
|
||||
cloudSettingsLogger.info("Settings loaded from cloud successfully");
|
||||
if (shouldNotify)
|
||||
|
|
|
@ -47,6 +47,7 @@ export let Paginator: t.Paginator;
|
|||
export let ScrollerThin: t.ScrollerThin;
|
||||
export let Clickable: t.Clickable;
|
||||
export let Avatar: t.Avatar;
|
||||
export let FocusLock: t.FocusLock;
|
||||
// token lagger real
|
||||
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
|
||||
export let useToken: t.useToken;
|
||||
|
@ -58,6 +59,6 @@ export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"
|
|||
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
|
||||
|
||||
waitFor(["FormItem", "Button"], m => {
|
||||
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar } = m);
|
||||
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar, FocusLock } = m);
|
||||
Forms = m;
|
||||
});
|
||||
|
|
|
@ -46,7 +46,7 @@ export let ReadStateStore: GenericStore;
|
|||
export let PresenceStore: GenericStore;
|
||||
export let PoggerModeSettingsStore: GenericStore;
|
||||
|
||||
export let GuildStore: Stores.GuildStore & t.FluxStore;
|
||||
export let GuildStore: t.GuildStore;
|
||||
export let UserStore: Stores.UserStore & t.FluxStore;
|
||||
export let UserProfileStore: GenericStore;
|
||||
export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore;
|
||||
|
|
4
src/webpack/common/types/components.d.ts
vendored
4
src/webpack/common/types/components.d.ts
vendored
|
@ -453,3 +453,7 @@ export type Avatar = ComponentType<PropsWithChildren<{
|
|||
"aria-hidden"?: boolean;
|
||||
"aria-label"?: string;
|
||||
}>>;
|
||||
|
||||
type FocusLock = ComponentType<PropsWithChildren<{
|
||||
containerRef: RefObject<HTMLElement>
|
||||
}>>;
|
||||
|
|
12
src/webpack/common/types/stores.d.ts
vendored
12
src/webpack/common/types/stores.d.ts
vendored
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { DraftType } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
import { Channel, Guild, Role } from "discord-types/general";
|
||||
|
||||
import { FluxDispatcher, FluxEvents } from "./utils";
|
||||
|
||||
|
@ -172,3 +172,13 @@ export class DraftStore extends FluxStore {
|
|||
getThreadDraftWithParentMessageId?(arg: any): any;
|
||||
getThreadSettings(channelId: string): any | null;
|
||||
}
|
||||
|
||||
export class GuildStore extends FluxStore {
|
||||
getGuild(guildId: string): Guild;
|
||||
getGuildCount(): number;
|
||||
getGuilds(): Record<string, Guild>;
|
||||
getGuildIds(): string[];
|
||||
getRole(guildId: string, roleId: string): Role;
|
||||
getRoles(guildId: string): Record<string, Role>;
|
||||
getAllGuildRoles(): Record<string, Record<string, Role>>;
|
||||
}
|
||||
|
|
6
src/webpack/common/types/utils.d.ts
vendored
6
src/webpack/common/types/utils.d.ts
vendored
|
@ -81,11 +81,7 @@ interface RestRequestData {
|
|||
retries?: number;
|
||||
}
|
||||
|
||||
export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>> & {
|
||||
V6OrEarlierAPIError: Error;
|
||||
V8APIError: Error;
|
||||
getAPIBaseURL(withVersion?: boolean): string;
|
||||
};
|
||||
export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>>;
|
||||
|
||||
export type Permissions = "CREATE_INSTANT_INVITE"
|
||||
| "KICK_MEMBERS"
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import type { Channel, User } from "discord-types/general";
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, waitFor } from "../webpack";
|
||||
import { _resolveReady, filters, findByCodeLazy, findByProps, findByPropsLazy, findLazy, proxyLazyWebpack, waitFor } from "../webpack";
|
||||
import type * as t from "./types/utils";
|
||||
|
||||
export let FluxDispatcher: t.FluxDispatcher;
|
||||
|
@ -37,7 +37,10 @@ export let ComponentDispatch;
|
|||
waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch);
|
||||
|
||||
|
||||
export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get");
|
||||
export const RestAPI: t.RestAPI = proxyLazyWebpack(() => {
|
||||
const mod = findByProps("getAPIBaseURL");
|
||||
return mod.HTTP ?? mod;
|
||||
});
|
||||
export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear");
|
||||
|
||||
export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage");
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"esnext.asynciterable",
|
||||
"esnext.symbol"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
|
@ -20,13 +20,15 @@
|
|||
|
||||
"baseUrl": "./src/",
|
||||
"paths": {
|
||||
"@main/*": ["./main/*"],
|
||||
"@api/*": ["./api/*"],
|
||||
"@components/*": ["./components/*"],
|
||||
"@utils/*": ["./utils/*"],
|
||||
"@shared/*": ["./shared/*"],
|
||||
"@webpack/types": ["./webpack/common/types"],
|
||||
"@webpack/common": ["./webpack/common"],
|
||||
"@webpack": ["./webpack/webpack"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*", "browser/**/*", "scripts/**/*"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue