mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-19 22:20:34 +00:00
Merge branch 'main' into plugin/memberListActivities
This commit is contained in:
commit
3cfefad3f5
127 changed files with 2462 additions and 873 deletions
|
@ -26,6 +26,7 @@ import { debounce } from "../src/utils";
|
||||||
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||||
import { getTheme, Theme } from "../src/utils/discord";
|
import { getTheme, Theme } from "../src/utils/discord";
|
||||||
import { getThemeInfo } from "../src/main/themes";
|
import { getThemeInfo } from "../src/main/themes";
|
||||||
|
import { Settings } from "../src/Vencord";
|
||||||
|
|
||||||
// Discord deletes this so need to store in variable
|
// Discord deletes this so need to store in variable
|
||||||
const { localStorage } = window;
|
const { localStorage } = window;
|
||||||
|
@ -96,8 +97,15 @@ window.VencordNative = {
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
get: () => localStorage.getItem("VencordSettings") || "{}",
|
get: () => {
|
||||||
set: async (s: string) => localStorage.setItem("VencordSettings", s),
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem("VencordSettings") || "{}");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse settings from localStorage: ", e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)),
|
||||||
getSettingsDir: async () => "LocalStorage"
|
getSettingsDir: async () => "LocalStorage"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
|
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
|
||||||
> No support will be provided for installing in this fashion. If you cannot figure it out, you should just stick to a regular install.
|
> No support will be provided for installing in this fashion. If you cannot figure it out, you should just stick to a regular install.
|
||||||
|
|
||||||
# Installation Guide
|
# Installation Guide
|
||||||
|
|
||||||
|
@ -95,5 +95,3 @@ Simply run:
|
||||||
```shell
|
```shell
|
||||||
pnpm uninject
|
pnpm uninject
|
||||||
```
|
```
|
||||||
|
|
||||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.7.0",
|
"version": "1.7.4",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
@ -67,7 +67,8 @@ const IGNORED_DISCORD_ERRORS = [
|
||||||
"Unable to process domain list delta: Client revision number is null",
|
"Unable to process domain list delta: Client revision number is null",
|
||||||
"Downloading the full bad domains file",
|
"Downloading the full bad domains file",
|
||||||
/\[GatewaySocket\].{0,110}Cannot access '/,
|
/\[GatewaySocket\].{0,110}Cannot access '/,
|
||||||
"search for 'name' in undefined"
|
"search for 'name' in undefined",
|
||||||
|
"Attempting to set fast connect zstd when unsupported"
|
||||||
] as Array<string | RegExp>;
|
] as Array<string | RegExp>;
|
||||||
|
|
||||||
function toCodeBlock(s: string) {
|
function toCodeBlock(s: string) {
|
||||||
|
|
|
@ -4,11 +4,12 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IpcEvents } from "@utils/IpcEvents";
|
import { PluginIpcMappings } from "@main/ipcPlugins";
|
||||||
|
import type { UserThemeHeader } from "@main/themes";
|
||||||
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { IpcRes } from "@utils/types";
|
import { IpcRes } from "@utils/types";
|
||||||
|
import type { Settings } from "api/Settings";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { PluginIpcMappings } from "main/ipcPlugins";
|
|
||||||
import type { UserThemeHeader } from "main/themes";
|
|
||||||
|
|
||||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||||
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
||||||
|
@ -46,8 +47,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
get: () => sendSync<string>(IpcEvents.GET_SETTINGS),
|
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
||||||
set: (settings: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings),
|
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
|
||||||
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
|
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -17,22 +17,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { Menu, React } from "@webpack/common";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
type ContextMenuPatchCallbackReturn = (() => void) | void;
|
|
||||||
/**
|
/**
|
||||||
* @param children The rendered context menu elements
|
* @param children The rendered context menu elements
|
||||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||||
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
|
|
||||||
*/
|
*/
|
||||||
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
|
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => void;
|
||||||
/**
|
/**
|
||||||
* @param navId The navId of the context menu being patched
|
* @param navId The navId of the context menu being patched
|
||||||
* @param children The rendered context menu elements
|
* @param children The rendered context menu elements
|
||||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||||
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
|
|
||||||
*/
|
*/
|
||||||
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
|
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => void;
|
||||||
|
|
||||||
const ContextMenuLogger = new Logger("ContextMenu");
|
const ContextMenuLogger = new Logger("ContextMenu");
|
||||||
|
|
||||||
|
@ -93,14 +91,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
|
||||||
* @param id The id of the child. If an array is specified, all ids will be tried
|
* @param id The id of the child. If an array is specified, all ids will be tried
|
||||||
* @param children The context menu children
|
* @param children The context menu children
|
||||||
*/
|
*/
|
||||||
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>, _itemsArray?: Array<ReactElement | null>): Array<ReactElement | null> | null {
|
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>): Array<ReactElement | null> | null {
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (child == null) continue;
|
if (child == null) continue;
|
||||||
|
|
||||||
|
if (Array.isArray(child)) {
|
||||||
|
const found = findGroupChildrenByChildId(id, child);
|
||||||
|
if (found !== null) return found;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(Array.isArray(id) && id.some(id => child.props?.id === id))
|
(Array.isArray(id) && id.some(id => child.props?.id === id))
|
||||||
|| child.props?.id === id
|
|| child.props?.id === id
|
||||||
) return _itemsArray ?? null;
|
) return children;
|
||||||
|
|
||||||
let nextChildren = child.props?.children;
|
let nextChildren = child.props?.children;
|
||||||
if (nextChildren) {
|
if (nextChildren) {
|
||||||
|
@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
|
||||||
child.props.children = nextChildren;
|
child.props.children = nextChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
|
const found = findGroupChildrenByChildId(id, nextChildren);
|
||||||
if (found !== null) return found;
|
if (found !== null) return found;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,9 +129,12 @@ interface ContextMenuProps {
|
||||||
onClose: (callback: (...args: Array<any>) => any) => void;
|
onClose: (callback: (...args: Array<any>) => any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchedMenus = new WeakSet();
|
export function _usePatchContextMenu(props: ContextMenuProps) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
children: cloneMenuChildren(props.children),
|
||||||
|
};
|
||||||
|
|
||||||
export function _patchContextMenu(props: ContextMenuProps) {
|
|
||||||
props.contextMenuApiArguments ??= [];
|
props.contextMenuApiArguments ??= [];
|
||||||
const contextMenuPatches = navPatches.get(props.navId);
|
const contextMenuPatches = navPatches.get(props.navId);
|
||||||
|
|
||||||
|
@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) {
|
||||||
if (contextMenuPatches) {
|
if (contextMenuPatches) {
|
||||||
for (const patch of contextMenuPatches) {
|
for (const patch of contextMenuPatches) {
|
||||||
try {
|
try {
|
||||||
const callback = patch(props.children, ...props.contextMenuApiArguments);
|
patch(props.children, ...props.contextMenuApiArguments);
|
||||||
if (!patchedMenus.has(props)) callback?.();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
||||||
}
|
}
|
||||||
|
@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) {
|
||||||
|
|
||||||
for (const patch of globalPatches) {
|
for (const patch of globalPatches) {
|
||||||
try {
|
try {
|
||||||
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
||||||
if (!patchedMenus.has(props)) callback?.();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ContextMenuLogger.error("Global patch errored,", err);
|
ContextMenuLogger.error("Global patch errored,", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
patchedMenus.add(props);
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneMenuChildren(obj: ReactElement | Array<ReactElement | null> | null) {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(cloneMenuChildren);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (React.isValidElement(obj)) {
|
||||||
|
obj = React.cloneElement(obj);
|
||||||
|
|
||||||
|
if (
|
||||||
|
obj?.props?.children &&
|
||||||
|
(obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null)
|
||||||
|
) {
|
||||||
|
obj.props.children = cloneMenuChildren(obj.props.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
|
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
||||||
import { localStorage } from "@utils/localStorage";
|
import { localStorage } from "@utils/localStorage";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/misc";
|
||||||
|
@ -52,7 +53,6 @@ export interface Settings {
|
||||||
| "under-page"
|
| "under-page"
|
||||||
| "window"
|
| "window"
|
||||||
| undefined;
|
| undefined;
|
||||||
macosTranslucency: boolean | undefined;
|
|
||||||
disableMinSize: boolean;
|
disableMinSize: boolean;
|
||||||
winNativeTitleBar: boolean;
|
winNativeTitleBar: boolean;
|
||||||
plugins: {
|
plugins: {
|
||||||
|
@ -88,8 +88,6 @@ const DefaultSettings: Settings = {
|
||||||
frameless: false,
|
frameless: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
winCtrlQ: false,
|
winCtrlQ: false,
|
||||||
// Replaced by macosVibrancyStyle
|
|
||||||
macosTranslucency: undefined,
|
|
||||||
macosVibrancyStyle: undefined,
|
macosVibrancyStyle: undefined,
|
||||||
disableMinSize: false,
|
disableMinSize: false,
|
||||||
winNativeTitleBar: false,
|
winNativeTitleBar: false,
|
||||||
|
@ -110,13 +108,8 @@ const DefaultSettings: Settings = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const settings = VencordNative.settings.get();
|
||||||
var settings = JSON.parse(VencordNative.settings.get()) as Settings;
|
mergeDefaults(settings, DefaultSettings);
|
||||||
mergeDefaults(settings, DefaultSettings);
|
|
||||||
} catch (err) {
|
|
||||||
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
|
||||||
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveSettingsOnFrequentAction = debounce(async () => {
|
const saveSettingsOnFrequentAction = debounce(async () => {
|
||||||
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
||||||
|
@ -125,76 +118,52 @@ const saveSettingsOnFrequentAction = debounce(async () => {
|
||||||
}
|
}
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array<string>; };
|
|
||||||
const subscriptions = new Set<SubscriptionCallback>();
|
|
||||||
|
|
||||||
const proxyCache = {} as Record<string, any>;
|
export const SettingsStore = new SettingsStoreClass(settings, {
|
||||||
|
readOnly: true,
|
||||||
|
getDefaultValue({
|
||||||
|
target,
|
||||||
|
key,
|
||||||
|
path
|
||||||
|
}) {
|
||||||
|
const v = target[key];
|
||||||
|
if (!plugins) return v; // plugins not initialised yet. this means this path was reached by being called on the top level
|
||||||
|
|
||||||
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
if (path === "plugins" && key in plugins)
|
||||||
function makeProxy(settings: any, root = settings, path = ""): Settings {
|
return target[key] = {
|
||||||
return proxyCache[path] ??= new Proxy(settings, {
|
enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
|
||||||
get(target, p: string) {
|
};
|
||||||
const v = target[p];
|
|
||||||
|
|
||||||
// using "in" is important in the following cases to properly handle falsy or nullish values
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
if (!(p in target)) {
|
// the default value.
|
||||||
// Return empty for plugins with no settings
|
if (path.startsWith("plugins.")) {
|
||||||
if (path === "plugins" && p in plugins)
|
const plugin = path.slice("plugins.".length);
|
||||||
return target[p] = makeProxy({
|
if (plugin in plugins) {
|
||||||
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
|
const setting = plugins[plugin].options?.[key];
|
||||||
}, root, `plugins.${p}`);
|
if (!setting) return v;
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
if ("default" in setting)
|
||||||
// the default value.
|
// normal setting with a default value
|
||||||
if (path.startsWith("plugins.")) {
|
return (target[key] = setting.default);
|
||||||
const plugin = path.slice("plugins.".length);
|
|
||||||
if (plugin in plugins) {
|
|
||||||
const setting = plugins[plugin].options?.[p];
|
|
||||||
if (!setting) return v;
|
|
||||||
if ("default" in setting)
|
|
||||||
// normal setting with a default value
|
|
||||||
return (target[p] = setting.default);
|
|
||||||
if (setting.type === OptionType.SELECT) {
|
|
||||||
const def = setting.options.find(o => o.default);
|
|
||||||
if (def)
|
|
||||||
target[p] = def.value;
|
|
||||||
return def?.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively proxy Objects with the updated property path
|
if (setting.type === OptionType.SELECT) {
|
||||||
if (typeof v === "object" && !Array.isArray(v) && v !== null)
|
const def = setting.options.find(o => o.default);
|
||||||
return makeProxy(v, root, `${path}${path && "."}${p}`);
|
if (def)
|
||||||
|
target[key] = def.value;
|
||||||
// primitive or similar, no need to proxy further
|
return def?.value;
|
||||||
return v;
|
|
||||||
},
|
|
||||||
|
|
||||||
set(target, p: string, v) {
|
|
||||||
// avoid unnecessary updates to React Components and other listeners
|
|
||||||
if (target[p] === v) return true;
|
|
||||||
|
|
||||||
target[p] = v;
|
|
||||||
// Call any listeners that are listening to a setting of this path
|
|
||||||
const setPath = `${path}${path && "."}${p}`;
|
|
||||||
delete proxyCache[setPath];
|
|
||||||
for (const subscription of subscriptions) {
|
|
||||||
if (!subscription._paths || subscription._paths.includes(setPath)) {
|
|
||||||
subscription(v, setPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And don't forget to persist the settings!
|
|
||||||
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
|
||||||
localStorage.Vencord_settingsDirty = true;
|
|
||||||
saveSettingsOnFrequentAction();
|
|
||||||
VencordNative.settings.set(JSON.stringify(root, null, 4));
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
});
|
return v;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SettingsStore.addGlobalChangeListener((_, path) => {
|
||||||
|
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
||||||
|
localStorage.Vencord_settingsDirty = true;
|
||||||
|
saveSettingsOnFrequentAction();
|
||||||
|
VencordNative.settings.set(SettingsStore.plain, path);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
||||||
|
@ -210,7 +179,7 @@ export const PlainSettings = settings;
|
||||||
* the updated settings to disk.
|
* the updated settings to disk.
|
||||||
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
||||||
*/
|
*/
|
||||||
export const Settings = makeProxy(settings);
|
export const Settings = SettingsStore.store;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings hook for React components. Returns a smart settings
|
* Settings hook for React components. Returns a smart settings
|
||||||
|
@ -223,43 +192,21 @@ export const Settings = makeProxy(settings);
|
||||||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
export function useSettings(paths?: UseSettings<Settings>[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
const onUpdate: SubscriptionCallback = paths
|
|
||||||
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
|
|
||||||
: forceUpdate;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.add(onUpdate);
|
if (paths) {
|
||||||
return () => void subscriptions.delete(onUpdate);
|
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
|
||||||
|
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
|
||||||
|
} else {
|
||||||
|
SettingsStore.addGlobalChangeListener(forceUpdate);
|
||||||
|
return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return Settings;
|
return SettingsStore.store;
|
||||||
}
|
|
||||||
|
|
||||||
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
|
||||||
type ResolvePropDeep<T, P> = P extends "" ? T :
|
|
||||||
P extends `${infer Pre}.${infer Suf}` ?
|
|
||||||
Pre extends keyof T ? ResolvePropDeep<T[Pre], Suf> : never : P extends keyof T ? T[P] : never;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a settings listener that will be invoked whenever the desired setting is updated
|
|
||||||
* @param path Path to the setting that you want to watch, for example "plugins.Unindent.enabled" will fire your callback
|
|
||||||
* whenever Unindent is toggled. Pass an empty string to get notified for all changes
|
|
||||||
* @param onUpdate Callback function whenever a setting matching path is updated. It gets passed the new value and the path
|
|
||||||
* to the updated setting. This path will be the same as your path argument, unless it was an empty string.
|
|
||||||
*
|
|
||||||
* @example addSettingsListener("", (newValue, path) => console.log(`${path} is now ${newValue}`))
|
|
||||||
* addSettingsListener("plugins.Unindent.enabled", v => console.log("Unindent is now", v ? "enabled" : "disabled"))
|
|
||||||
*/
|
|
||||||
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
|
|
||||||
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
|
|
||||||
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
|
|
||||||
if (path)
|
|
||||||
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
|
|
||||||
subscriptions.add(onUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||||
const { plugins } = settings;
|
const { plugins } = SettingsStore.plain;
|
||||||
if (name in plugins) return;
|
if (name in plugins) return;
|
||||||
|
|
||||||
for (const oldName of oldNames) {
|
for (const oldName of oldNames) {
|
||||||
|
@ -267,7 +214,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||||
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
||||||
plugins[name] = plugins[oldName];
|
plugins[name] = plugins[oldName];
|
||||||
delete plugins[oldName];
|
delete plugins[oldName];
|
||||||
VencordNative.settings.set(JSON.stringify(settings, null, 4));
|
SettingsStore.markAsChanged();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,13 @@
|
||||||
|
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { CodeBlock } from "@components/CodeBlock";
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "@utils/text";
|
||||||
import { ReplaceFn } from "@utils/types";
|
import { Patch, ReplaceFn } from "@utils/types";
|
||||||
import { search } from "@webpack";
|
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";
|
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() {
|
function PatchHelper() {
|
||||||
const [find, setFind] = React.useState<string>("");
|
const [find, setFind] = React.useState<string>("");
|
||||||
const [match, setMatch] = React.useState<string>("");
|
const [match, setMatch] = React.useState<string>("");
|
||||||
|
@ -260,6 +314,13 @@ function PatchHelper() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab title="Patch Helper">
|
<SettingsTab title="Patch Helper">
|
||||||
|
<Forms.FormTitle>full patch</Forms.FormTitle>
|
||||||
|
<FullPatchInput
|
||||||
|
setFind={onFindChange}
|
||||||
|
setMatch={onMatchChange}
|
||||||
|
setReplacement={setReplacement}
|
||||||
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle>find</Forms.FormTitle>
|
<Forms.FormTitle>find</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { Flex } from "@components/Flex";
|
||||||
import { DeleteIcon } from "@components/Icons";
|
import { DeleteIcon } from "@components/Icons";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||||
|
import type { UserThemeHeader } from "@main/themes";
|
||||||
import { openInviteModal } from "@utils/discord";
|
import { openInviteModal } from "@utils/discord";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
|
@ -30,7 +31,6 @@ import { showItemInFolder } from "@utils/native";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { findByPropsLazy, findLazy } from "@webpack";
|
import { findByPropsLazy, findLazy } from "@webpack";
|
||||||
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||||
import { UserThemeHeader } from "main/themes";
|
|
||||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||||
|
|
||||||
import { AddonCard } from "./AddonCard";
|
import { AddonCard } from "./AddonCard";
|
||||||
|
|
|
@ -50,14 +50,6 @@ function VencordSettings() {
|
||||||
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
||||||
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
||||||
|
|
||||||
// One-time migration of the old setting to the new one if necessary.
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (settings.macosTranslucency === true && !settings.macosVibrancyStyle) {
|
|
||||||
settings.macosVibrancyStyle = "sidebar";
|
|
||||||
settings.macosTranslucency = undefined;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const Switches: Array<false | {
|
const Switches: Array<false | {
|
||||||
key: KeysOfType<typeof settings, boolean>;
|
key: KeysOfType<typeof settings, boolean>;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -164,7 +156,7 @@ function VencordSettings() {
|
||||||
options={[
|
options={[
|
||||||
// Sorted from most opaque to most transparent
|
// Sorted from most opaque to most transparent
|
||||||
{
|
{
|
||||||
label: "No vibrancy", default: !settings.macosTranslucency, value: undefined
|
label: "No vibrancy", value: undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Under Page (window tinting)",
|
label: "Under Page (window tinting)",
|
||||||
|
@ -191,9 +183,8 @@ function VencordSettings() {
|
||||||
value: "header"
|
value: "header"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Sidebar (old value for transparent windows)",
|
label: "Sidebar",
|
||||||
value: "sidebar",
|
value: "sidebar"
|
||||||
default: settings.macosTranslucency
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Tooltip",
|
label: "Tooltip",
|
||||||
|
|
|
@ -19,7 +19,8 @@
|
||||||
import { app, protocol, session } from "electron";
|
import { app, protocol, session } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { ensureSafePath, getSettings } from "./ipcMain";
|
import { ensureSafePath } from "./ipcMain";
|
||||||
|
import { RendererSettings } from "./settings";
|
||||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||||
import { installExt } from "./utils/extensions";
|
import { installExt } from "./utils/extensions";
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (getSettings().enableReactDevtools)
|
if (RendererSettings.store.enableReactDevtools)
|
||||||
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
||||||
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
||||||
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
||||||
|
|
|
@ -18,22 +18,21 @@
|
||||||
|
|
||||||
import "./updater";
|
import "./updater";
|
||||||
import "./ipcPlugins";
|
import "./ipcPlugins";
|
||||||
|
import "./settings";
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { IpcEvents } from "@utils/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { Queue } from "@utils/Queue";
|
|
||||||
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
|
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
|
||||||
import { mkdirSync, readFileSync, watch } from "fs";
|
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
|
||||||
import { open, readdir, readFile, writeFile } from "fs/promises";
|
import { open, readdir, readFile } from "fs/promises";
|
||||||
import { join, normalize } from "path";
|
import { join, normalize } from "path";
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/monacoWin.html;base64";
|
import monacoHtml from "~fileContent/monacoWin.html;base64";
|
||||||
|
|
||||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
|
||||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||||
|
|
||||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
||||||
mkdirSync(THEMES_DIR, { recursive: true });
|
mkdirSync(THEMES_DIR, { recursive: true });
|
||||||
|
|
||||||
export function ensureSafePath(basePath: string, path: string) {
|
export function ensureSafePath(basePath: string, path: string) {
|
||||||
|
@ -71,22 +70,6 @@ function getThemeData(fileName: string) {
|
||||||
return readFile(safePath, "utf-8");
|
return readFile(safePath, "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readSettings() {
|
|
||||||
try {
|
|
||||||
return readFileSync(SETTINGS_FILE, "utf-8");
|
|
||||||
} catch {
|
|
||||||
return "{}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSettings(): typeof import("@api/Settings").Settings {
|
|
||||||
try {
|
|
||||||
return JSON.parse(readSettings());
|
|
||||||
} catch {
|
|
||||||
return {} as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||||
|
@ -101,12 +84,10 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cssWriteQueue = new Queue();
|
|
||||||
const settingsWriteQueue = new Queue();
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
||||||
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
||||||
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
|
writeFileSync(QUICKCSS_PATH, css)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
|
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
|
||||||
|
@ -117,25 +98,25 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
|
||||||
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
|
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
|
||||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
|
||||||
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export function initIpc(mainWindow: BrowserWindow) {
|
export function initIpc(mainWindow: BrowserWindow) {
|
||||||
|
let quickCssWatcher: FSWatcher | undefined;
|
||||||
|
|
||||||
open(QUICKCSS_PATH, "a+").then(fd => {
|
open(QUICKCSS_PATH, "a+").then(fd => {
|
||||||
fd.close();
|
fd.close();
|
||||||
watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
|
quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
|
||||||
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
|
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
|
||||||
}, 50));
|
}, 50));
|
||||||
});
|
}).catch(() => { });
|
||||||
|
|
||||||
watch(THEMES_DIR, { persistent: false }, debounce(() => {
|
const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => {
|
||||||
mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0);
|
mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
mainWindow.once("closed", () => {
|
||||||
|
quickCssWatcher?.close();
|
||||||
|
themesWatcher.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IpcEvents } from "@utils/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
import PluginNatives from "~pluginNatives";
|
import PluginNatives from "~pluginNatives";
|
||||||
|
|
|
@ -16,11 +16,12 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { onceDefined } from "@utils/onceDefined";
|
import { onceDefined } from "@shared/onceDefined";
|
||||||
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
|
|
||||||
import { getSettings, initIpc } from "./ipcMain";
|
import { initIpc } from "./ipcMain";
|
||||||
|
import { RendererSettings } from "./settings";
|
||||||
import { IS_VANILLA } from "./utils/constants";
|
import { IS_VANILLA } from "./utils/constants";
|
||||||
|
|
||||||
console.log("[Vencord] Starting up...");
|
console.log("[Vencord] Starting up...");
|
||||||
|
@ -41,8 +42,7 @@ require.main!.filename = join(asarPath, discordPkg.main);
|
||||||
app.setAppPath(asarPath);
|
app.setAppPath(asarPath);
|
||||||
|
|
||||||
if (!IS_VANILLA) {
|
if (!IS_VANILLA) {
|
||||||
const settings = getSettings();
|
const settings = RendererSettings.store;
|
||||||
|
|
||||||
// Repatch after host updates on Windows
|
// Repatch after host updates on Windows
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
require("./patchWin32Updater");
|
require("./patchWin32Updater");
|
||||||
|
@ -84,13 +84,11 @@ if (!IS_VANILLA) {
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency);
|
const needsVibrancy = process.platform === "darwin" && settings.macosVibrancyStyle;
|
||||||
|
|
||||||
if (needsVibrancy) {
|
if (needsVibrancy) {
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
if (settings.macosTranslucency) {
|
if (settings.macosVibrancyStyle) {
|
||||||
options.vibrancy = "sidebar";
|
|
||||||
} else if (settings.macosVibrancyStyle) {
|
|
||||||
options.vibrancy = settings.macosVibrancyStyle;
|
options.vibrancy = settings.macosVibrancyStyle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
53
src/main/settings.ts
Normal file
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/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IpcEvents } from "@utils/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { execFile as cpExecFile } from "child_process";
|
import { execFile as cpExecFile } from "child_process";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
@ -49,9 +49,12 @@ async function getRepo() {
|
||||||
async function calculateGitChanges() {
|
async function calculateGitChanges() {
|
||||||
await git("fetch");
|
await git("fetch");
|
||||||
|
|
||||||
const branch = await git("branch", "--show-current");
|
const branch = (await git("branch", "--show-current")).stdout.trim();
|
||||||
|
|
||||||
const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s");
|
const existsOnOrigin = (await git("ls-remote", "origin", branch)).stdout.length > 0;
|
||||||
|
if (!existsOnOrigin) return [];
|
||||||
|
|
||||||
|
const res = await git("log", `HEAD...origin/${branch}`, "--pretty=format:%an/%h/%s");
|
||||||
|
|
||||||
const commits = res.stdout.trim();
|
const commits = res.stdout.trim();
|
||||||
return commits ? commits.split("\n").map(line => {
|
return commits ? commits.split("\n").map(line => {
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { VENCORD_USER_AGENT } from "@utils/constants";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { IpcEvents } from "@utils/IpcEvents";
|
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { writeFile } from "fs/promises";
|
import { writeFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
@ -53,7 +53,7 @@ async function calculateGitChanges() {
|
||||||
// github api only sends the long sha
|
// github api only sends the long sha
|
||||||
hash: c.sha.slice(0, 7),
|
hash: c.sha.slice(0, 7),
|
||||||
author: c.author.login,
|
author: c.author.login,
|
||||||
message: c.commit.message
|
message: c.commit.message.substring(c.commit.message.indexOf("\n") + 1)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ export const SETTINGS_DIR = join(DATA_DIR, "settings");
|
||||||
export const THEMES_DIR = join(DATA_DIR, "themes");
|
export const THEMES_DIR = join(DATA_DIR, "themes");
|
||||||
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
||||||
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
||||||
|
export const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, "native-settings.json");
|
||||||
export const ALLOWED_PROTOCOLS = [
|
export const ALLOWED_PROTOCOLS = [
|
||||||
"https:",
|
"https:",
|
||||||
"http:",
|
"http:",
|
||||||
|
|
|
@ -13,10 +13,10 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
patches: [{
|
patches: [{
|
||||||
find: 'location:"ChannelTextAreaButtons"',
|
find: '"sticker")',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /if\(!\i\.isMobile\)\{(?=.+?&&(\i)\.push\(.{0,50}"gift")/,
|
match: /!\i\.isMobile(?=.+?(\i)\.push\(.{0,50}"gift")/,
|
||||||
replace: "$&Vencord.Api.ChatButtons._injectButtons($1,arguments[0]);"
|
replace: "$& &&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)"
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,15 +22,15 @@ import definePlugin from "@utils/types";
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ContextMenuAPI",
|
name: "ContextMenuAPI",
|
||||||
description: "API for adding/removing items to/from context menus.",
|
description: "API for adding/removing items to/from context menus.",
|
||||||
authors: [Devs.Nuckyz, Devs.Ven],
|
authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi],
|
||||||
required: true,
|
required: true,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "♫ (つ。◕‿‿◕。)つ ♪",
|
find: "♫ (つ。◕‿‿◕。)つ ♪",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /let{navId:/,
|
match: /(?=let{navId:)(?<=function \i\((\i)\).+?)/,
|
||||||
replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&"
|
replace: "$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default definePlugin({
|
||||||
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
|
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
|
||||||
replace: "$&vencordProps=$1,"
|
replace: "$&vencordProps=$1,"
|
||||||
}, {
|
}, {
|
||||||
match: /decorators:.{0,100}?children:\[/,
|
match: /\.Messages\.GUILD_OWNER(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
|
||||||
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
|
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".handleSendMessage=",
|
find: ".handleSendMessage",
|
||||||
replacement: {
|
replacement: {
|
||||||
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
|
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
|
||||||
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
|
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default definePlugin({
|
||||||
required: true,
|
required: true,
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: 'displayName="NoticeStore"',
|
find: '"NoticeStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
|
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
|
||||||
|
|
|
@ -16,11 +16,10 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch } from "@api/ContextMenu";
|
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { React, SettingsRouter } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
|
@ -30,23 +29,6 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven, Devs.Megu],
|
authors: [Devs.Ven, Devs.Megu],
|
||||||
required: true,
|
required: true,
|
||||||
|
|
||||||
start() {
|
|
||||||
// The settings shortcuts in the user settings cog context menu
|
|
||||||
// read the elements from a hardcoded map which for obvious reason
|
|
||||||
// doesn't contain our sections. This patches the actions of our
|
|
||||||
// sections to manually use SettingsRouter (which only works on desktop
|
|
||||||
// but the context menu is usually not available on mobile anyway)
|
|
||||||
addContextMenuPatch("user-settings-cog", children => () => {
|
|
||||||
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any;
|
|
||||||
section?.forEach(c => {
|
|
||||||
const id = c?.props?.id;
|
|
||||||
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
|
|
||||||
c.props.action = () => SettingsRouter.open(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
patches: [{
|
patches: [{
|
||||||
find: ".versionHash",
|
find: ".versionHash",
|
||||||
replacement: [
|
replacement: [
|
||||||
|
@ -75,6 +57,12 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
replace: "...$self.makeSettingsCategories($1),$&"
|
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)[],
|
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
|
||||||
|
|
|
@ -16,27 +16,46 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
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({
|
export default definePlugin({
|
||||||
name: "AlwaysTrust",
|
name: "AlwaysTrust",
|
||||||
description: "Removes the annoying untrusted domain and suspicious file popup",
|
description: "Removes the annoying untrusted domain and suspicious file popup",
|
||||||
authors: [Devs.zt],
|
authors: [Devs.zt, Devs.Trwy],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".displayName=\"MaskedLinkStore\"",
|
find: '="MaskedLinkStore",',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/,
|
match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/,
|
||||||
replace: "return true"
|
replace: "return true"
|
||||||
}
|
},
|
||||||
|
predicate: () => settings.store.domain
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "isSuspiciousDownload:",
|
find: "isSuspiciousDownload:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
|
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
|
||||||
replace: "$&return null;"
|
replace: "$&return null;"
|
||||||
}
|
},
|
||||||
|
predicate: () => settings.store.file
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
settings
|
||||||
});
|
});
|
||||||
|
|
|
@ -67,7 +67,7 @@ const settings = definePluginSettings({
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "AnonymiseFileNames",
|
name: "AnonymiseFileNames",
|
||||||
authors: [Devs.obscurity],
|
authors: [Devs.fawn],
|
||||||
description: "Anonymise uploaded file names",
|
description: "Anonymise uploaded file names",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
@ -78,6 +78,13 @@ export default definePlugin({
|
||||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
"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",
|
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
|
||||||
replacement: {
|
replacement: {
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default definePlugin({
|
||||||
"Change GIF alt text from simply being 'GIF' to containing the gif tags / filename",
|
"Change GIF alt text from simply being 'GIF' to containing the gif tags / filename",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "onCloseImage=",
|
find: '"onCloseImage",',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
||||||
replace:
|
replace:
|
||||||
|
|
|
@ -15,8 +15,8 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".GIFPickerResultTypes.SEARCH",
|
find: ".GIFPickerResultTypes.SEARCH",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: "this.state={resultType:null}",
|
match: /(?<="state",{resultType:)null/,
|
||||||
replace: 'this.state={resultType:"Favorites"}'
|
replace: '"Favorites"'
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { canonicalizeMatch } from "@utils/patches";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
|
||||||
|
@ -39,8 +40,12 @@ export default definePlugin({
|
||||||
match: /hideNote:.+?(?=([,}].*?\)))/g,
|
match: /hideNote:.+?(?=([,}].*?\)))/g,
|
||||||
replace: (m, rest) => {
|
replace: (m, rest) => {
|
||||||
const destructuringMatch = rest.match(/}=.+/);
|
const destructuringMatch = rest.match(/}=.+/);
|
||||||
if (destructuringMatch == null) return "hideNote:!0";
|
if (destructuringMatch) {
|
||||||
return m;
|
const defaultValueMatch = m.match(canonicalizeMatch(/hideNote:(\i)=!?\d/));
|
||||||
|
return defaultValueMatch ? `hideNote:${defaultValueMatch[1]}=!0` : m;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "hideNote:!0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -52,10 +57,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".Messages.NOTE}",
|
find: ".popularApplicationCommandIds,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=return \i\?)null(?=:\(0,\i\.jsxs)/,
|
match: /lastSection:(!?\i)}\),/,
|
||||||
replace: "$self.patchPadding(arguments[0])"
|
replace: "$&$self.patchPadding($1),"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -75,8 +80,8 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
patchPadding(e: any) {
|
patchPadding(lastSection: any) {
|
||||||
if (!e.lastSection) return;
|
if (!lastSection) return;
|
||||||
return (
|
return (
|
||||||
<div className={UserPopoutSectionCssClasses.lastSection}></div>
|
<div className={UserPopoutSectionCssClasses.lastSection}></div>
|
||||||
);
|
);
|
||||||
|
|
6
src/plugins/betterRoleContext/README.md
Normal file
6
src/plugins/betterRoleContext/README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# BetterRoleContext
|
||||||
|
|
||||||
|
Adds options to copy role color and edit role when right clicking roles in the user profile
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326)
|
||||||
|
|
81
src/plugins/betterRoleContext/index.tsx
Normal file
81
src/plugins/betterRoleContext/index.tsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { getCurrentGuild } from "@utils/discord";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
||||||
|
|
||||||
|
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||||
|
|
||||||
|
function PencilIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path fill="currentColor" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppearanceIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M 12,0 C 5.3733333,0 0,5.3733333 0,12 c 0,6.626667 5.3733333,12 12,12 1.106667,0 2,-0.893333 2,-2 0,-0.52 -0.2,-0.986667 -0.52,-1.346667 -0.306667,-0.346666 -0.506667,-0.813333 -0.506667,-1.32 0,-1.106666 0.893334,-2 2,-2 h 2.36 C 21.013333,17.333333 24,14.346667 24,10.666667 24,4.7733333 18.626667,0 12,0 Z M 4.6666667,12 c -1.1066667,0 -2,-0.893333 -2,-2 0,-1.1066667 0.8933333,-2 2,-2 1.1066666,0 2,0.8933333 2,2 0,1.106667 -0.8933334,2 -2,2 z M 8.666667,6.6666667 c -1.106667,0 -2.0000003,-0.8933334 -2.0000003,-2 0,-1.1066667 0.8933333,-2 2.0000003,-2 1.106666,0 2,0.8933333 2,2 0,1.1066666 -0.893334,2 -2,2 z m 6.666666,0 c -1.106666,0 -2,-0.8933334 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.1066666 -0.893333,2 -2,2 z m 4,5.3333333 c -1.106666,0 -2,-0.893333 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.106667 -0.893333,2 -2,2 z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "BetterRoleContext",
|
||||||
|
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
start() {
|
||||||
|
// DeveloperMode needs to be enabled for the context menu to be shown
|
||||||
|
TextAndImagesSettingsStores.DeveloperMode.updateSetting(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
contextMenus: {
|
||||||
|
"dev-context"(children, { id }: { id: string; }) {
|
||||||
|
const guild = getCurrentGuild();
|
||||||
|
if (!guild) return;
|
||||||
|
|
||||||
|
const role = GuildStore.getRole(guild.id, id);
|
||||||
|
if (!role) return;
|
||||||
|
|
||||||
|
if (role.colorString) {
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-copy-role-color"
|
||||||
|
label="Copy Role Color"
|
||||||
|
action={() => Clipboard.copy(role.colorString!)}
|
||||||
|
icon={AppearanceIcon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-edit-role"
|
||||||
|
label="Edit Role"
|
||||||
|
action={async () => {
|
||||||
|
await GuildSettingsActions.open(guild.id, "ROLES");
|
||||||
|
GuildSettingsActions.selectRole(id);
|
||||||
|
}}
|
||||||
|
icon={PencilIcon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
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 TOC eagerly
|
||||||
|
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
|
||||||
|
replace: "(async ()=>$2)(),"
|
||||||
|
},
|
||||||
|
predicate: () => settings.store.eagerLoad
|
||||||
|
},
|
||||||
|
{ // Settings cog context menu
|
||||||
|
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||||
|
replacement: {
|
||||||
|
match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||||
|
replace: "$self.wrapMenu($&)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
Layer(props: LayerProps) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary fallback={() => props.children as any}>
|
||||||
|
<Layer {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
wrapMenu(list: SettingsEntry[]) {
|
||||||
|
if (!settings.store.organizeMenu) return list;
|
||||||
|
|
||||||
|
const items = [{ label: null as string | null, items: [] as SettingsEntry[] }];
|
||||||
|
|
||||||
|
for (const item of list) {
|
||||||
|
if (item.section === "HEADER") {
|
||||||
|
items.push({ label: item.label, items: [] });
|
||||||
|
} else if (item.section === "DIVIDER") {
|
||||||
|
items.push({ label: i18n.Messages.OTHER_OPTIONS, items: [] });
|
||||||
|
} else {
|
||||||
|
items.at(-1)!.items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filter(predicate: (item: SettingsEntry) => boolean) {
|
||||||
|
for (const category of items) {
|
||||||
|
category.items = category.items.filter(predicate);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
map(render: (item: SettingsEntry) => ReactElement) {
|
||||||
|
return items
|
||||||
|
.filter(a => a.items.length > 0)
|
||||||
|
.map(({ label, items }) => {
|
||||||
|
const children = items.map(render);
|
||||||
|
if (label) {
|
||||||
|
return (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id={label.replace(/\W/, "_")}
|
||||||
|
label={label}
|
||||||
|
children={children}
|
||||||
|
action={children[0].props.action}
|
||||||
|
/>);
|
||||||
|
} else {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
|
@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterUploadButton",
|
name: "BetterUploadButton",
|
||||||
authors: [Devs.obscurity, Devs.Ven],
|
authors: [Devs.fawn, Devs.Ven],
|
||||||
description: "Upload with a single click, open menu with right click",
|
description: "Upload with a single click, open menu with right click",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { ScreenshareIcon } from "@components/Icons";
|
import { ScreenshareIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { openImageModal } from "@utils/discord";
|
import { openImageModal } from "@utils/discord";
|
||||||
|
@ -60,7 +60,7 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica
|
||||||
openImageModal(previewUrl);
|
openImageModal(previewUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => () => {
|
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => {
|
||||||
const stream = ApplicationStreamingStore.getAnyStreamForUser(userId);
|
const stream = ApplicationStreamingStore.getAnyStreamForUser(userId);
|
||||||
if (!stream) return;
|
if (!stream) return;
|
||||||
|
|
||||||
|
@ -89,12 +89,8 @@ export default definePlugin({
|
||||||
name: "BiggerStreamPreview",
|
name: "BiggerStreamPreview",
|
||||||
description: "This plugin allows you to enlarge stream previews",
|
description: "This plugin allows you to enlarge stream previews",
|
||||||
authors: [Devs.phil],
|
authors: [Devs.phil],
|
||||||
start: () => {
|
contextMenus: {
|
||||||
addContextMenuPatch("user-context", userContextPatch);
|
"user-context": userContextPatch,
|
||||||
addContextMenuPatch("stream-context", streamContextPatch);
|
"stream-context": streamContextPatch
|
||||||
},
|
|
||||||
stop: () => {
|
|
||||||
removeContextMenuPatch("user-context", userContextPatch);
|
|
||||||
removeContextMenuPatch("stream-context", streamContextPatch);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||||
import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common";
|
import { Button, Forms, useStateFromStores } from "@webpack/common";
|
||||||
|
|
||||||
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||||
|
|
||||||
|
@ -200,8 +200,8 @@ function captureOne(str, regex) {
|
||||||
return (result === null) ? null : result[1];
|
return (result === null) ? null : result[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapReject(arr, mapFunc, rejectFunc = _.isNull) {
|
function mapReject(arr, mapFunc) {
|
||||||
return _.reject(arr.map(mapFunc), rejectFunc);
|
return arr.map(mapFunc).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateColorVars(color: string) {
|
function updateColorVars(color: string) {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { LinkIcon } from "@components/Icons";
|
import { LinkIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
@ -29,7 +29,7 @@ interface UserContextProps {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
|
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
|
@ -46,12 +46,7 @@ export default definePlugin({
|
||||||
name: "CopyUserURLs",
|
name: "CopyUserURLs",
|
||||||
authors: [Devs.castdrian],
|
authors: [Devs.castdrian],
|
||||||
description: "Adds a 'Copy User URL' option to the user context menu.",
|
description: "Adds a 'Copy User URL' option to the user context menu.",
|
||||||
|
contextMenus: {
|
||||||
start() {
|
"user-context": UserContextMenuPatch
|
||||||
addContextMenuPatch("user-context", UserContextMenuPatch);
|
}
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("user-context", UserContextMenuPatch);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -175,7 +175,7 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
startTime: {
|
startTime: {
|
||||||
type: OptionType.NUMBER,
|
type: OptionType.NUMBER,
|
||||||
description: "Start timestamp (only for custom timestamp mode)",
|
description: "Start timestamp in milisecond (only for custom timestamp mode)",
|
||||||
onChange: onChange,
|
onChange: onChange,
|
||||||
disabled: isTimestampDisabled,
|
disabled: isTimestampDisabled,
|
||||||
isValid: (value: number) => {
|
isValid: (value: number) => {
|
||||||
|
@ -185,7 +185,7 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
endTime: {
|
endTime: {
|
||||||
type: OptionType.NUMBER,
|
type: OptionType.NUMBER,
|
||||||
description: "End timestamp (only for custom timestamp mode)",
|
description: "End timestamp in milisecond (only for custom timestamp mode)",
|
||||||
onChange: onChange,
|
onChange: onChange,
|
||||||
disabled: isTimestampDisabled,
|
disabled: isTimestampDisabled,
|
||||||
isValid: (value: number) => {
|
isValid: (value: number) => {
|
||||||
|
@ -313,12 +313,12 @@ async function createActivity(): Promise<Activity | undefined> {
|
||||||
switch (settings.store.timestampMode) {
|
switch (settings.store.timestampMode) {
|
||||||
case TimestampMode.NOW:
|
case TimestampMode.NOW:
|
||||||
activity.timestamps = {
|
activity.timestamps = {
|
||||||
start: Math.floor(Date.now() / 1000)
|
start: Date.now()
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case TimestampMode.TIME:
|
case TimestampMode.TIME:
|
||||||
activity.timestamps = {
|
activity.timestamps = {
|
||||||
start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds()
|
start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case TimestampMode.CUSTOM:
|
case TimestampMode.CUSTOM:
|
||||||
|
|
|
@ -43,13 +43,13 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: "DefaultCustomizationSections",
|
find: "DefaultCustomizationSections",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<={user:\i},"decoration"\),)/,
|
match: /(?<=USER_SETTINGS_AVATAR_DECORATION},"decoration"\),)/,
|
||||||
replace: "$self.DecorSection(),"
|
replace: "$self.DecorSection(),"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Decoration modal module
|
// Decoration modal module
|
||||||
{
|
{
|
||||||
find: ".decorationGridItem",
|
find: ".decorationGridItem,",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/,
|
match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/,
|
||||||
|
@ -61,8 +61,8 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// Remove NEW label from decor avatar decorations
|
// Remove NEW label from decor avatar decorations
|
||||||
{
|
{
|
||||||
match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i;if\()(?<=avatarDecoration:(\i).+?)/,
|
match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i)(?<=avatarDecoration:(\i).+?)/,
|
||||||
replace: "$1.skuId===$self.SKU_ID||"
|
replace: "||$1.skuId===$self.SKU_ID"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -131,9 +131,10 @@ export default definePlugin({
|
||||||
getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {
|
getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {
|
||||||
// Only Decor avatar decorations have this SKU ID
|
// Only Decor avatar decorations have this SKU ID
|
||||||
if (avatarDecoration?.skuId === SKU_ID) {
|
if (avatarDecoration?.skuId === SKU_ID) {
|
||||||
const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`);
|
const parts = avatarDecoration.asset.split("_");
|
||||||
url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString());
|
// Remove a_ prefix if it's animated and animation is disabled
|
||||||
return url.toString();
|
if (isAnimatedAvatarDecoration(avatarDecoration.asset) && !canAnimate) parts.shift();
|
||||||
|
return `${CDN_URL}/${parts.join("_")}.png`;
|
||||||
} else if (avatarDecoration?.skuId === RAW_SKU_ID) {
|
} else if (avatarDecoration?.skuId === RAW_SKU_ID) {
|
||||||
return avatarDecoration.asset;
|
return avatarDecoration.asset;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { proxyLazy } from "@utils/lazy";
|
import { proxyLazy } from "@utils/lazy";
|
||||||
import { useEffect, useState, zustandCreate } from "@webpack/common";
|
import { useEffect, useState, zustandCreate } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /,?(?=this\.idleTimeout=new \i\.Timeout)/,
|
match: /,?(?=\i\(this,"idleTimeout",new \i\.Timeout\))/,
|
||||||
replace: ";return;"
|
replace: ";return;"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
|
@ -56,7 +56,7 @@ function getUrl(data: Data) {
|
||||||
if (data.t === "Emoji")
|
if (data.t === "Emoji")
|
||||||
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`;
|
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) {
|
async function fetchSticker(id: string) {
|
||||||
|
@ -312,7 +312,7 @@ function isGifUrl(url: string) {
|
||||||
return new URL(url).pathname.endsWith(".gif");
|
return new URL(url).pathname.endsWith(".gif");
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
|
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
||||||
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
|
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
|
||||||
|
|
||||||
if (!favoriteableId) return;
|
if (!favoriteableId) return;
|
||||||
|
@ -341,7 +341,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
|
||||||
findGroupChildrenByChildId("copy-link", children)?.push(menuItem);
|
findGroupChildrenByChildId("copy-link", children)?.push(menuItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => {
|
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
|
||||||
const { id, name, type } = props?.target?.dataset ?? {};
|
const { id, name, type } = props?.target?.dataset ?? {};
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
|
@ -363,14 +363,8 @@ export default definePlugin({
|
||||||
description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
|
description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
|
||||||
tags: ["StickerCloner"],
|
tags: ["StickerCloner"],
|
||||||
authors: [Devs.Ven, Devs.Nuckyz],
|
authors: [Devs.Ven, Devs.Nuckyz],
|
||||||
|
contextMenus: {
|
||||||
start() {
|
"message": messageContextMenuPatch,
|
||||||
addContextMenuPatch("message", messageContextMenuPatch);
|
"expression-picker": expressionPickerPatch
|
||||||
addContextMenuPatch("expression-picker", expressionPickerPatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
removeContextMenuPatch("expression-picker", expressionPickerPatch);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".isStaff=()",
|
find: '"isStaff",',
|
||||||
predicate: () => settings.store.enableIsStaff,
|
predicate: () => settings.store.enableIsStaff,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -162,7 +162,7 @@ const settings = definePluginSettings({
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
hyperLinkText: {
|
hyperLinkText: {
|
||||||
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji name.",
|
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.",
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
default: "{{NAME}}"
|
default: "{{NAME}}"
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FakeNitro",
|
name: "FakeNitro",
|
||||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
||||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '.displayName="UserSettingsProtoStore"',
|
find: '"UserSettingsProtoStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Overwrite incoming connection settings proto with our local settings
|
// Overwrite incoming connection settings proto with our local settings
|
||||||
|
@ -388,6 +388,14 @@ export default definePlugin({
|
||||||
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
|
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
|
||||||
replace: "true"
|
replace: "true"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// Make all Soundboard sounds available
|
||||||
|
{
|
||||||
|
find: 'type:"GUILD_SOUNDBOARD_SOUND_CREATE"',
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g,
|
||||||
|
replace: "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -585,13 +593,15 @@ export default definePlugin({
|
||||||
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
|
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
|
||||||
|
|
||||||
children = this.clearEmptyArrayItems(children);
|
children = this.clearEmptyArrayItems(children);
|
||||||
this.trimContent(children);
|
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return modifyChildren(lodash.cloneDeep(content));
|
const newContent = modifyChildren(lodash.cloneDeep(content));
|
||||||
|
this.trimContent(newContent);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
new Logger("FakeNitro").error(err);
|
new Logger("FakeNitro").error(err);
|
||||||
return content;
|
return content;
|
||||||
|
@ -791,8 +801,8 @@ export default definePlugin({
|
||||||
title: "Hold on!",
|
title: "Hold on!",
|
||||||
body: <div>
|
body: <div>
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
You are trying to send/edit a message that contains a FakeNitro emoji or sticker
|
You are trying to send/edit a message that contains a FakeNitro emoji or sticker,
|
||||||
, however you do not have permissions to embed links in the current channel.
|
however you do not have permissions to embed links in the current channel.
|
||||||
Are you sure you want to send this message? Your FakeNitro items will appear as a link only.
|
Are you sure you want to send this message? Your FakeNitro items will appear as a link only.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
|
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
|
||||||
|
@ -864,7 +874,9 @@ export default definePlugin({
|
||||||
const url = new URL(link);
|
const url = new URL(link);
|
||||||
url.searchParams.set("name", sticker.name);
|
url.searchParams.set("name", sticker.name);
|
||||||
|
|
||||||
messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${sticker.name}](${url})` : url}`;
|
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", sticker.name);
|
||||||
|
|
||||||
|
messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`;
|
||||||
extra.stickers!.length = 0;
|
extra.stickers!.length = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,7 +200,14 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
|
||||||
|
|
||||||
|
|
||||||
export function getTargetString(urlStr: string) {
|
export function getTargetString(urlStr: string) {
|
||||||
const url = new URL(urlStr);
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(urlStr);
|
||||||
|
} catch (err) {
|
||||||
|
// Can't resolve URL, return as-is
|
||||||
|
return urlStr;
|
||||||
|
}
|
||||||
|
|
||||||
switch (settings.store.searchOption) {
|
switch (settings.store.searchOption) {
|
||||||
case "url":
|
case "url":
|
||||||
return url.href;
|
return url.href;
|
||||||
|
|
|
@ -4,14 +4,14 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { RendererSettings } from "@main/settings";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { getSettings } from "main/ipcMain";
|
|
||||||
|
|
||||||
app.on("browser-window-created", (_, win) => {
|
app.on("browser-window-created", (_, win) => {
|
||||||
win.webContents.on("frame-created", (_, { frame }) => {
|
win.webContents.on("frame-created", (_, { frame }) => {
|
||||||
frame.once("dom-ready", () => {
|
frame.once("dom-ready", () => {
|
||||||
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
||||||
const settings = getSettings().plugins?.FixSpotifyEmbeds;
|
const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds;
|
||||||
if (!settings?.enabled) return;
|
if (!settings?.enabled) return;
|
||||||
|
|
||||||
frame.executeJavaScript(`
|
frame.executeJavaScript(`
|
||||||
|
|
|
@ -4,14 +4,14 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { RendererSettings } from "@main/settings";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { getSettings } from "main/ipcMain";
|
|
||||||
|
|
||||||
app.on("browser-window-created", (_, win) => {
|
app.on("browser-window-created", (_, win) => {
|
||||||
win.webContents.on("frame-created", (_, { frame }) => {
|
win.webContents.on("frame-created", (_, { frame }) => {
|
||||||
frame.once("dom-ready", () => {
|
frame.once("dom-ready", () => {
|
||||||
if (frame.url.startsWith("https://www.youtube.com/")) {
|
if (frame.url.startsWith("https://www.youtube.com/")) {
|
||||||
const settings = getSettings().plugins?.FixYoutubeEmbeds;
|
const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds;
|
||||||
if (!settings?.enabled) return;
|
if (!settings?.enabled) return;
|
||||||
|
|
||||||
frame.executeJavaScript(`
|
frame.executeJavaScript(`
|
||||||
|
|
5
src/plugins/friendsSince/README.md
Normal file
5
src/plugins/friendsSince/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# FriendsSince
|
||||||
|
|
||||||
|
Shows when you became friends with someone in the user popout
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/45497981/bb258188-ab48-4c4d-9858-1e90ba41e926)
|
60
src/plugins/friendsSince/index.tsx
Normal file
60
src/plugins/friendsSince/index.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { React, RelationshipStore } from "@webpack/common";
|
||||||
|
|
||||||
|
const { Heading, Text } = findByPropsLazy("Heading", "Text");
|
||||||
|
const container = findByPropsLazy("memberSinceContainer");
|
||||||
|
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
|
||||||
|
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");
|
||||||
|
const locale = findByPropsLazy("getLocale");
|
||||||
|
const lastSection = findByPropsLazy("lastSection");
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "FriendsSince",
|
||||||
|
description: "Shows when you became friends with someone in the user popout",
|
||||||
|
authors: [Devs.Elvyra],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".AnalyticsSections.USER_PROFILE}",
|
||||||
|
replacement: {
|
||||||
|
match: /\i.default,\{userId:(\i.id).{0,30}}\)/,
|
||||||
|
replace: "$&,$self.friendsSince({ userId: $1 })"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
|
||||||
|
replacement: {
|
||||||
|
match: /\i.default,\{userId:(\i)}\)/,
|
||||||
|
replace: "$&,$self.friendsSince({ userId: $1 })"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
friendsSince: ErrorBoundary.wrap(({ userId }: { userId: string; }) => {
|
||||||
|
const friendsSince = RelationshipStore.getSince(userId);
|
||||||
|
if (!friendsSince) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={lastSection.section}>
|
||||||
|
<Heading variant="eyebrow" className={clydeMoreInfo.title}>
|
||||||
|
Friends Since
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div className={container.memberSinceContainer}>
|
||||||
|
<Text variant="text-sm/normal" className={clydeMoreInfo.body}>
|
||||||
|
{getCreatedAtDate(friendsSince, locale.getLocale())}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, { noop: true })
|
||||||
|
});
|
||||||
|
|
|
@ -29,10 +29,10 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
patches: [{
|
patches: [{
|
||||||
find: ".handleSelectGIF=",
|
find: '"handleSelectGIF",',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\.handleSelectGIF=(\i)=>\{/,
|
match: /"handleSelectGIF",(\i)=>\{/,
|
||||||
replace: ".handleSelectGIF=$1=>{if (!this.props.className) return $self.handleSelect($1);"
|
replace: '"handleSelectGIF",$1=>{if (!this.props.className) return $self.handleSelect($1);'
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings, Settings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import { Margins } from "@utils/margins";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findStoreLazy } from "@webpack";
|
import { findStoreLazy } from "@webpack";
|
||||||
import { StatusSettingsStores, Tooltip } from "webpack/common";
|
import { Button, Forms, showToast, StatusSettingsStores, TextInput, Toasts, Tooltip, useEffect, useState } from "webpack/common";
|
||||||
|
|
||||||
const enum ActivitiesTypes {
|
const enum ActivitiesTypes {
|
||||||
Game,
|
Game,
|
||||||
|
@ -69,7 +71,113 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
StatusSettingsStores.ShowCurrentGame.updateSetting(old => old);
|
StatusSettingsStores.ShowCurrentGame.updateSetting(old => old);
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = definePluginSettings({}).withPrivateSettings<{
|
function ImportCustomRPCComponent() {
|
||||||
|
return (
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the allowed list</Forms.FormText>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const id = Settings.plugins.CustomRPC?.appID as string | undefined;
|
||||||
|
if (!id) {
|
||||||
|
return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlreadyAdded = allowedIdsPushID?.(id);
|
||||||
|
if (isAlreadyAdded) {
|
||||||
|
showToast("CustomRPC application ID is already added.", Toasts.Type.FAILURE);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import CustomRPC ID
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let allowedIdsPushID: ((id: string) => boolean) | null = null;
|
||||||
|
|
||||||
|
function AllowedIdsComponent(props: { setValue: (value: string) => void; }) {
|
||||||
|
const [allowedIds, setAllowedIds] = useState<string>(settings.store.allowedIds ?? "");
|
||||||
|
|
||||||
|
allowedIdsPushID = (id: string) => {
|
||||||
|
const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean));
|
||||||
|
|
||||||
|
const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);
|
||||||
|
|
||||||
|
const ids = Array.from(currentIds).join(", ");
|
||||||
|
setAllowedIds(ids);
|
||||||
|
props.setValue(ids);
|
||||||
|
|
||||||
|
return isAlreadyAdded;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
allowedIdsPushID = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function handleChange(newValue: string) {
|
||||||
|
setAllowedIds(newValue);
|
||||||
|
props.setValue(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle tag="h3">Allowed List</Forms.FormTitle>
|
||||||
|
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC)</Forms.FormText>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
value={allowedIds}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="235834946571337729, 343383572805058560"
|
||||||
|
/>
|
||||||
|
</Forms.FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
importCustomRPC: {
|
||||||
|
type: OptionType.COMPONENT,
|
||||||
|
description: "",
|
||||||
|
component: () => <ImportCustomRPCComponent />
|
||||||
|
},
|
||||||
|
allowedIds: {
|
||||||
|
type: OptionType.COMPONENT,
|
||||||
|
description: "",
|
||||||
|
default: "",
|
||||||
|
onChange(newValue: string) {
|
||||||
|
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
|
||||||
|
settings.store.allowedIds = Array.from(ids).join(", ");
|
||||||
|
},
|
||||||
|
component: props => <AllowedIdsComponent setValue={props.setValue} />
|
||||||
|
},
|
||||||
|
ignorePlaying: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Ignore all playing activities (These are usually game and RPC activities)",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
ignoreStreaming: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Ignore all streaming activities",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
ignoreListening: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Ignore all listening activities (These are usually spotify activities)",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
ignoreWatching: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Ignore all watching activities",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
ignoreCompeting: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Ignore all competing activities (These are normally special game activities)",
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}).withPrivateSettings<{
|
||||||
ignoredActivities: IgnoredActivity[];
|
ignoredActivities: IgnoredActivity[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -77,19 +185,35 @@ function getIgnoredActivities() {
|
||||||
return settings.store.ignoredActivities ??= [];
|
return settings.store.ignoredActivities ??= [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isActivityTypeIgnored(type: number, id?: string) {
|
||||||
|
if (id && settings.store.allowedIds.includes(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 0: return settings.store.ignorePlaying;
|
||||||
|
case 1: return settings.store.ignoreStreaming;
|
||||||
|
case 2: return settings.store.ignoreListening;
|
||||||
|
case 3: return settings.store.ignoreWatching;
|
||||||
|
case 5: return settings.store.ignoreCompeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "IgnoreActivities",
|
name: "IgnoreActivities",
|
||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz],
|
||||||
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are ignored from the Registered Games and Activities tabs.",
|
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.",
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '.displayName="LocalActivityStore"',
|
find: '="LocalActivityStore",',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /HANG_STATUS.+?(?=!\i\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
|
match: /HANG_STATUS.+?(?=!\i\(\)\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
|
||||||
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
|
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -141,13 +265,17 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
|
isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
|
||||||
if (props.type === 0 || props.type === 3) {
|
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
|
||||||
if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id);
|
|
||||||
else {
|
if (props.application_id != null) {
|
||||||
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
|
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id);
|
||||||
if (exePath) return !getIgnoredActivities().some(activity => activity.id === exePath);
|
} else {
|
||||||
|
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
|
||||||
|
if (exePath) {
|
||||||
|
return !getIgnoredActivities().some(activity => activity.id === exePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -16,14 +16,14 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
import { disableStyle, enableStyle } from "@api/Styles";
|
||||||
import { makeRange } from "@components/PluginSettings/components";
|
import { makeRange } from "@components/PluginSettings/components";
|
||||||
|
import { debounce } from "@shared/debounce";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { debounce } from "@utils/debounce";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { ContextMenuApi, Menu, React, ReactDOM } from "@webpack/common";
|
import { Menu, ReactDOM } from "@webpack/common";
|
||||||
import type { Root } from "react-dom/client";
|
import type { Root } from "react-dom/client";
|
||||||
|
|
||||||
import { Magnifier, MagnifierProps } from "./components/Magnifier";
|
import { Magnifier, MagnifierProps } from "./components/Magnifier";
|
||||||
|
@ -80,25 +80,25 @@ export const settings = definePluginSettings({
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
const imageContextMenuPatch: NavContextMenuPatchCallback = children => {
|
||||||
|
const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]);
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
<Menu.MenuGroup id="image-zoom">
|
<Menu.MenuGroup id="image-zoom">
|
||||||
<Menu.MenuCheckboxItem
|
<Menu.MenuCheckboxItem
|
||||||
id="vc-square"
|
id="vc-square"
|
||||||
label="Square Lens"
|
label="Square Lens"
|
||||||
checked={settings.store.square}
|
checked={square}
|
||||||
action={() => {
|
action={() => {
|
||||||
settings.store.square = !settings.store.square;
|
settings.store.square = !square;
|
||||||
ContextMenuApi.closeContextMenu();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Menu.MenuCheckboxItem
|
<Menu.MenuCheckboxItem
|
||||||
id="vc-nearest-neighbour"
|
id="vc-nearest-neighbour"
|
||||||
label="Nearest Neighbour"
|
label="Nearest Neighbour"
|
||||||
checked={settings.store.nearestNeighbour}
|
checked={nearestNeighbour}
|
||||||
action={() => {
|
action={() => {
|
||||||
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
|
settings.store.nearestNeighbour = !nearestNeighbour;
|
||||||
ContextMenuApi.closeContextMenu();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Menu.MenuControlItem
|
<Menu.MenuControlItem
|
||||||
|
@ -168,7 +168,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
find: "handleImageLoad=",
|
find: ".handleImageLoad)",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /placeholderVersion:\i,/,
|
match: /placeholderVersion:\i,/,
|
||||||
|
@ -196,6 +196,9 @@ export default definePlugin({
|
||||||
],
|
],
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
|
contextMenus: {
|
||||||
|
"image-context": imageContextMenuPatch
|
||||||
|
},
|
||||||
|
|
||||||
// to stop from rendering twice /shrug
|
// to stop from rendering twice /shrug
|
||||||
currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,
|
currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,
|
||||||
|
@ -245,7 +248,6 @@ export default definePlugin({
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
enableStyle(styles);
|
enableStyle(styles);
|
||||||
addContextMenuPatch("image-context", imageContextMenuPatch);
|
|
||||||
this.element = document.createElement("div");
|
this.element = document.createElement("div");
|
||||||
this.element.classList.add("MagnifierContainer");
|
this.element.classList.add("MagnifierContainer");
|
||||||
document.body.appendChild(this.element);
|
document.body.appendChild(this.element);
|
||||||
|
@ -256,6 +258,5 @@ export default definePlugin({
|
||||||
// so componenetWillUnMount gets called if Magnifier component is still alive
|
// so componenetWillUnMount gets called if Magnifier component is still alive
|
||||||
this.root && this.root.unmount();
|
this.root && this.root.unmount();
|
||||||
this.element?.remove();
|
this.element?.remove();
|
||||||
removeContextMenuPatch("image-context", imageContextMenuPatch);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { registerCommand, unregisterCommand } from "@api/Commands";
|
import { registerCommand, unregisterCommand } from "@api/Commands";
|
||||||
|
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Patch, Plugin, StartAt } from "@utils/types";
|
import { Patch, Plugin, StartAt } from "@utils/types";
|
||||||
|
@ -119,7 +120,7 @@ export function startDependenciesRecursive(p: Plugin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
|
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
|
||||||
const { name, commands, flux } = p;
|
const { name, commands, flux, contextMenus } = p;
|
||||||
|
|
||||||
if (p.start) {
|
if (p.start) {
|
||||||
logger.info("Starting plugin", name);
|
logger.info("Starting plugin", name);
|
||||||
|
@ -154,11 +155,17 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contextMenus) {
|
||||||
|
for (const navId in contextMenus) {
|
||||||
|
addContextMenuPatch(navId, contextMenus[navId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, p => `startPlugin ${p.name}`);
|
}, p => `startPlugin ${p.name}`);
|
||||||
|
|
||||||
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
|
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
|
||||||
const { name, commands, flux } = p;
|
const { name, commands, flux, contextMenus } = p;
|
||||||
if (p.stop) {
|
if (p.stop) {
|
||||||
logger.info("Stopping plugin", name);
|
logger.info("Stopping plugin", name);
|
||||||
if (!p.started) {
|
if (!p.started) {
|
||||||
|
@ -192,5 +199,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contextMenus) {
|
||||||
|
for (const navId in contextMenus) {
|
||||||
|
removeContextMenuPatch(navId, contextMenus[navId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, p => `stopPlugin ${p.name}`);
|
}, p => `stopPlugin ${p.name}`);
|
||||||
|
|
|
@ -81,11 +81,11 @@ export default definePlugin({
|
||||||
find: ".LOADING_DID_YOU_KNOW}",
|
find: ".LOADING_DID_YOU_KNOW}",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /\._loadingText=function\(\)\{/,
|
match: /"_loadingText",function\(\)\{/,
|
||||||
replace: "$&return $self.quote;",
|
replace: "$&return $self.quote;",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /\._eventLoadingText=function\(\)\{/,
|
match: /"_eventLoadingText",function\(\)\{/,
|
||||||
replace: "$&return $self.quote;",
|
replace: "$&return $self.quote;",
|
||||||
predicate: () => settings.store.replaceEvents
|
predicate: () => settings.store.replaceEvents
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
||||||
|
|
||||||
const { groups } = useStateFromStores(
|
const { groups } = useStateFromStores(
|
||||||
[ChannelMemberStore],
|
[ChannelMemberStore],
|
||||||
() => ChannelMemberStore.getProps(guildId, currentChannel.id)
|
() => ChannelMemberStore.getProps(guildId, currentChannel?.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {
|
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {
|
||||||
|
|
|
@ -18,10 +18,11 @@
|
||||||
|
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findStoreLazy } from "@webpack";
|
import { findStoreLazy } from "@webpack";
|
||||||
import { FluxStore } from "@webpack/types";
|
import { FluxStore } from "@webpack/types";
|
||||||
|
|
||||||
|
@ -32,6 +33,21 @@ export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxSto
|
||||||
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
|
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
toolTip: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "If the member count should be displayed on the server tooltip",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
memberList: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "If the member count should be displayed on the member list",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const sharedIntlNumberFormat = new Intl.NumberFormat();
|
const sharedIntlNumberFormat = new Intl.NumberFormat();
|
||||||
export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
|
export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
|
||||||
export const cl = classNameFactory("vc-membercount-");
|
export const cl = classNameFactory("vc-membercount-");
|
||||||
|
@ -40,6 +56,7 @@ export default definePlugin({
|
||||||
name: "MemberCount",
|
name: "MemberCount",
|
||||||
description: "Shows the amount of online & total members in the server member list and tooltip",
|
description: "Shows the amount of online & total members in the server member list and tooltip",
|
||||||
authors: [Devs.Ven, Devs.Commandtechno],
|
authors: [Devs.Ven, Devs.Commandtechno],
|
||||||
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
@ -47,17 +64,18 @@ export default definePlugin({
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
|
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
|
||||||
replace: ":[$1?.startsWith('members')?$self.render():null,$2"
|
replace: ":[$1?.startsWith('members')?$self.render():null,$2"
|
||||||
}
|
},
|
||||||
|
predicate: () => settings.store.memberList
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".invitesDisabledTooltip",
|
find: ".invitesDisabledTooltip",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/,
|
match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/,
|
||||||
replace: ",$self.renderTooltip(arguments[0].guild)]"
|
replace: ",$self.renderTooltip(arguments[0].guild)]"
|
||||||
}
|
},
|
||||||
|
predicate: () => settings.store.toolTip
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
render: ErrorBoundary.wrap(MemberCount, { noop: true }),
|
render: ErrorBoundary.wrap(MemberCount, { noop: true }),
|
||||||
renderTooltip: ErrorBoundary.wrap(guild => <MemberCount isTooltip tooltipGuildId={guild.id} />, { noop: true })
|
renderTooltip: ErrorBoundary.wrap(guild => <MemberCount isTooltip tooltipGuildId={guild.id} />, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
@ -255,7 +255,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
|
||||||
delete msg.embeds;
|
delete msg.embeds;
|
||||||
delete msg.interaction;
|
delete msg.interaction;
|
||||||
|
|
||||||
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
|
messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)
|
||||||
.then(m => m && FluxDispatcher.dispatch({
|
.then(m => m && FluxDispatcher.dispatch({
|
||||||
type: "MESSAGE_UPDATE",
|
type: "MESSAGE_UPDATE",
|
||||||
message: msg
|
message: msg
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import "./messageLogger.css";
|
import "./messageLogger.css";
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
import { disableStyle, enableStyle } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
@ -45,7 +45,7 @@ function addDeleteStyle() {
|
||||||
|
|
||||||
const REMOVE_HISTORY_ID = "ml-remove-history";
|
const REMOVE_HISTORY_ID = "ml-remove-history";
|
||||||
const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style";
|
const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style";
|
||||||
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => {
|
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => {
|
||||||
const { message } = props;
|
const { message } = props;
|
||||||
const { deleted, editHistory, id, channel_id } = message;
|
const { deleted, editHistory, id, channel_id } = message;
|
||||||
|
|
||||||
|
@ -94,13 +94,12 @@ export default definePlugin({
|
||||||
description: "Temporarily logs deleted and edited messages.",
|
description: "Temporarily logs deleted and edited messages.",
|
||||||
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN],
|
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN],
|
||||||
|
|
||||||
start() {
|
contextMenus: {
|
||||||
addDeleteStyle();
|
"message": patchMessageContextMenu
|
||||||
addContextMenuPatch("message", patchMessageContextMenu);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
start() {
|
||||||
removeContextMenuPatch("message", patchMessageContextMenu);
|
addDeleteStyle();
|
||||||
},
|
},
|
||||||
|
|
||||||
renderEdit(edit: { timestamp: any, content: string; }) {
|
renderEdit(edit: { timestamp: any, content: string; }) {
|
||||||
|
@ -138,6 +137,16 @@ export default definePlugin({
|
||||||
],
|
],
|
||||||
onChange: () => addDeleteStyle()
|
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: {
|
ignoreBots: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Whether to ignore messages by bots",
|
description: "Whether to ignore messages by bots",
|
||||||
|
@ -198,8 +207,8 @@ export default definePlugin({
|
||||||
return cache;
|
return cache;
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldIgnore(message: any) {
|
shouldIgnore(message: any, isEdit = false) {
|
||||||
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
|
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds, logEdits, logDeletes } = Settings.plugins.MessageLogger;
|
||||||
const myId = UserStore.getCurrentUser().id;
|
const myId = UserStore.getCurrentUser().id;
|
||||||
|
|
||||||
return ignoreBots && message.author?.bot ||
|
return ignoreBots && message.author?.bot ||
|
||||||
|
@ -207,6 +216,7 @@ export default definePlugin({
|
||||||
ignoreUsers.includes(message.author?.id) ||
|
ignoreUsers.includes(message.author?.id) ||
|
||||||
ignoreChannels.includes(message.channel_id) ||
|
ignoreChannels.includes(message.channel_id) ||
|
||||||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
|
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
|
||||||
|
(isEdit ? !logEdits : !logDeletes) ||
|
||||||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
|
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -215,7 +225,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
// MessageStore
|
// MessageStore
|
||||||
// Module 171447
|
// Module 171447
|
||||||
find: "displayName=\"MessageStore\"",
|
find: '"MessageStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Add deleted=true to all target messages in the MESSAGE_DELETE event
|
// Add deleted=true to all target messages in the MESSAGE_DELETE event
|
||||||
|
@ -242,7 +252,7 @@ export default definePlugin({
|
||||||
match: /(MESSAGE_UPDATE:function\((\i)\).+?)\.update\((\i)/,
|
match: /(MESSAGE_UPDATE:function\((\i)\).+?)\.update\((\i)/,
|
||||||
replace: "$1" +
|
replace: "$1" +
|
||||||
".update($3,m =>" +
|
".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 ?" +
|
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
||||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
||||||
" m" +
|
" m" +
|
||||||
|
@ -370,7 +380,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
// ReferencedMessageStore
|
// ReferencedMessageStore
|
||||||
// Module 778667
|
// Module 778667
|
||||||
find: "displayName=\"ReferencedMessageStore\"",
|
find: '"ReferencedMessageStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /MESSAGE_DELETE:function\((\i)\).+?},/,
|
match: /MESSAGE_DELETE:function\((\i)\).+?},/,
|
||||||
|
|
|
@ -47,8 +47,8 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded
|
find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/,
|
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
|
||||||
replace: '($1||arguments[0].isCurrentUser)?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),'
|
replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -38,8 +38,8 @@ export default definePlugin({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...[
|
...[
|
||||||
'displayName="MessageStore"',
|
'="MessageStore",',
|
||||||
'displayName="ReadStateStore"'
|
'"displayName","ReadStateStore")'
|
||||||
].map(find => ({
|
].map(find => ({
|
||||||
find,
|
find,
|
||||||
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,
|
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: '.ensureModule("discord_rpc")',
|
find: '.ensureModule("discord_rpc")',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)\)}/,
|
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/,
|
||||||
replace: '.ensureModule("discord_rpc")}',
|
replace: '.ensureModule("discord_rpc")}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
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
|
||||||
|
});
|
|
@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
|
||||||
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||||
import { getUniqueUsername } from "@utils/discord";
|
import { getUniqueUsername } from "@utils/discord";
|
||||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
|
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||||
import type { Guild } from "discord-types/general";
|
import type { Guild } from "discord-types/general";
|
||||||
|
|
||||||
import { settings } from "..";
|
import { settings } from "..";
|
||||||
|
@ -78,6 +78,8 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
||||||
const [selectedItemIndex, selectItem] = useState(0);
|
const [selectedItemIndex, selectItem] = useState(0);
|
||||||
const selectedItem = permissions[selectedItemIndex];
|
const selectedItem = permissions[selectedItemIndex];
|
||||||
|
|
||||||
|
const roles = GuildStore.getRoles(guild.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot
|
<ModalRoot
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
|
@ -100,7 +102,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
||||||
<div className={cl("perms-list")}>
|
<div className={cl("perms-list")}>
|
||||||
{permissions.map((permission, index) => {
|
{permissions.map((permission, index) => {
|
||||||
const user = UserStore.getUser(permission.id ?? "");
|
const user = UserStore.getUser(permission.id ?? "");
|
||||||
const role = guild.roles[permission.id ?? ""];
|
const role = roles[permission.id ?? ""];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -201,7 +203,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
||||||
id="vc-pw-view-as-role"
|
id="vc-pw-view-as-role"
|
||||||
label="View As Role"
|
label="View As Role"
|
||||||
action={() => {
|
action={() => {
|
||||||
const role = guild.roles[roleId];
|
const role = GuildStore.getRole(guild.id, roleId);
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
@ -107,7 +107,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
permissions = Object.values(guild.roles).map(role => ({
|
permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
|
||||||
type: PermissionType.Role,
|
type: PermissionType.Role,
|
||||||
...role
|
...role
|
||||||
}));
|
}));
|
||||||
|
@ -125,10 +125,10 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
|
function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
|
||||||
return (children, props) => () => {
|
return (children, props) => {
|
||||||
if (!props) return;
|
if (!props) return;
|
||||||
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild)))
|
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild)))
|
||||||
return children;
|
return;
|
||||||
|
|
||||||
const group = findGroupChildrenByChildId(childId, children);
|
const group = findGroupChildrenByChildId(childId, children);
|
||||||
|
|
||||||
|
@ -173,19 +173,10 @@ export default definePlugin({
|
||||||
|
|
||||||
UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBoder} />,
|
UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBoder} />,
|
||||||
|
|
||||||
userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User),
|
contextMenus: {
|
||||||
channelContextMenuPatch: makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),
|
"user-context": makeContextMenuPatch("roles", MenuItemParentType.User),
|
||||||
guildContextMenuPatch: makeContextMenuPatch("privacy", MenuItemParentType.Guild),
|
"channel-context": makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),
|
||||||
|
"guild-context": makeContextMenuPatch("privacy", MenuItemParentType.Guild),
|
||||||
start() {
|
"guild-header-popout": makeContextMenuPatch("privacy", MenuItemParentType.Guild)
|
||||||
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
}
|
||||||
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
|
||||||
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
|
||||||
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
|
||||||
removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -67,7 +67,9 @@ export function getPermissionDescription(permission: string): ReactNode {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSortedRoles({ roles, id }: Guild, member: GuildMember) {
|
export function getSortedRoles({ id }: Guild, member: GuildMember) {
|
||||||
|
const roles = GuildStore.getRoles(id);
|
||||||
|
|
||||||
return [...member.roles, id]
|
return [...member.roles, id]
|
||||||
.map(id => roles[id])
|
.map(id => roles[id])
|
||||||
.sort((a, b) => b.position - a.position);
|
.sort((a, b) => b.position - a.position);
|
||||||
|
@ -85,13 +87,13 @@ export function sortUserRoles(roles: Role[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortPermissionOverwrites<T extends { id: string; type: number; }>(overwrites: T[], guildId: string) {
|
export function sortPermissionOverwrites<T extends { id: string; type: number; }>(overwrites: T[], guildId: string) {
|
||||||
const guild = GuildStore.getGuild(guildId);
|
const roles = GuildStore.getRoles(guildId);
|
||||||
|
|
||||||
return overwrites.sort((a, b) => {
|
return overwrites.sort((a, b) => {
|
||||||
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;
|
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;
|
||||||
|
|
||||||
const roleA = guild.roles[a.id];
|
const roleA = roles[a.id];
|
||||||
const roleB = guild.roles[b.id];
|
const roleB = roles[b.id];
|
||||||
|
|
||||||
return roleB.position - roleA.position;
|
return roleB.position - roleA.position;
|
||||||
});
|
});
|
||||||
|
|
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}Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\).{0,50}"UserSettings"/);
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-pindms-modal-");
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
categoryId: string | null;
|
||||||
|
initalChannelId: string | null;
|
||||||
|
modalProps: ModalProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCategory(categoryId: string | null, initalChannelId: string | null) {
|
||||||
|
const [category, setCategory] = useState<Category | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (categoryId)
|
||||||
|
setCategory(getCategory(categoryId)!);
|
||||||
|
else if (initalChannelId)
|
||||||
|
setCategory({
|
||||||
|
id: Toasts.genId(),
|
||||||
|
name: `Pin Category ${categories.length + 1}`,
|
||||||
|
color: DEFAULT_COLOR,
|
||||||
|
collapsed: false,
|
||||||
|
channels: [initalChannelId]
|
||||||
|
});
|
||||||
|
}, [categoryId, initalChannelId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
category,
|
||||||
|
setCategory
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) {
|
||||||
|
const { category, setCategory } = useCategory(categoryId, initalChannelId);
|
||||||
|
|
||||||
|
if (!category) return null;
|
||||||
|
|
||||||
|
const onSave = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!categoryId)
|
||||||
|
await createCategory(category);
|
||||||
|
else
|
||||||
|
await updateCategory(category);
|
||||||
|
|
||||||
|
forceUpdate();
|
||||||
|
modalProps.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalRoot {...modalProps}>
|
||||||
|
<ModalHeader>
|
||||||
|
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{categoryId ? "Edit" : "New"} Category</Text>
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
{/* form is here so when you press enter while in the text input it submits */}
|
||||||
|
<form onSubmit={onSave}>
|
||||||
|
<ModalContent className={cl("content")}>
|
||||||
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle>Name</Forms.FormTitle>
|
||||||
|
<TextInput
|
||||||
|
value={category.name}
|
||||||
|
onChange={e => setCategory({ ...category, name: e })}
|
||||||
|
/>
|
||||||
|
</Forms.FormSection>
|
||||||
|
<Forms.FormDivider />
|
||||||
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle>Color</Forms.FormTitle>
|
||||||
|
<ColorPickerWithSwatches
|
||||||
|
key={category.name}
|
||||||
|
defaultColor={DEFAULT_COLOR}
|
||||||
|
colors={SWATCHES}
|
||||||
|
onChange={c => setCategory({ ...category, color: c! })}
|
||||||
|
value={category.color}
|
||||||
|
renderDefaultButton={() => null}
|
||||||
|
renderCustomButton={() => (
|
||||||
|
<ColorPicker
|
||||||
|
color={category.color}
|
||||||
|
onChange={c => setCategory({ ...category, color: c! })}
|
||||||
|
key={category.name}
|
||||||
|
showEyeDropper={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</ModalContent>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button type="submit" onClick={onSave} disabled={!category.name}>{categoryId ? "Save" : "Create"}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
|
</ModalRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
|
||||||
|
openModalLazy(async () => {
|
||||||
|
await requireSettingsMenu();
|
||||||
|
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initalChannelId={channelId} />;
|
||||||
|
});
|
||||||
|
|
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, PinOrder, settings } from "../index";
|
||||||
|
import { openCategoryModal } from "./CreateCategoryModal";
|
||||||
|
|
||||||
|
function createPinMenuItem(channelId: string) {
|
||||||
|
const pinned = isPinned(channelId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="pin-dm"
|
||||||
|
label="Pin DMs"
|
||||||
|
>
|
||||||
|
|
||||||
|
{!pinned && (
|
||||||
|
<>
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-add-category"
|
||||||
|
label="Add Category"
|
||||||
|
color="brand"
|
||||||
|
action={() => openCategoryModal(null, channelId)}
|
||||||
|
/>
|
||||||
|
<Menu.MenuSeparator />
|
||||||
|
|
||||||
|
{
|
||||||
|
categories.map(category => (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id={`pin-category-${category.name}`}
|
||||||
|
label={category.name}
|
||||||
|
action={() => addChannelToCategory(channelId, category.id).then(forceUpdate)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pinned && (
|
||||||
|
<>
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="unpin-dm"
|
||||||
|
label="Unpin DM"
|
||||||
|
color="danger"
|
||||||
|
action={() => removeChannelFromCategory(channelId).then(forceUpdate)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
settings.store.pinOrder === PinOrder.Custom && canMoveChannelInDirection(channelId, -1) && (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="move-up"
|
||||||
|
label="Move Up"
|
||||||
|
action={() => moveChannel(channelId, -1).then(forceUpdate)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
settings.store.pinOrder === PinOrder.Custom && canMoveChannelInDirection(channelId, 1) && (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="move-down"
|
||||||
|
label="Move Down"
|
||||||
|
action={() => moveChannel(channelId, 1).then(forceUpdate)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Menu.MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => {
|
||||||
|
const container = findGroupChildrenByChildId("leave-channel", children);
|
||||||
|
container?.unshift(createPinMenuItem(props.channel.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserContext: NavContextMenuPatchCallback = (children, props) => {
|
||||||
|
const container = findGroupChildrenByChildId("close-dm", children);
|
||||||
|
if (container) {
|
||||||
|
const idx = container.findIndex(c => c?.props?.id === "close-dm");
|
||||||
|
container.splice(idx, 0, createPinMenuItem(props.channel.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contextMenus = {
|
||||||
|
"gdm-context": GroupDMContext,
|
||||||
|
"user-context": UserContext
|
||||||
|
};
|
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,75 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
|
||||||
import { Menu } from "@webpack/common";
|
|
||||||
|
|
||||||
import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings";
|
|
||||||
|
|
||||||
function PinMenuItem(channelId: string) {
|
|
||||||
const pinned = isPinned(channelId);
|
|
||||||
const canMove = pinned && settings.store.pinOrder === PinOrder.Custom;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Menu.MenuItem
|
|
||||||
id="pin-dm"
|
|
||||||
label={pinned ? "Unpin DM" : "Pin DM"}
|
|
||||||
action={() => togglePin(channelId)}
|
|
||||||
/>
|
|
||||||
{canMove && snapshotArray[0] !== channelId && (
|
|
||||||
<Menu.MenuItem
|
|
||||||
id="move-pin-up"
|
|
||||||
label="Move Pin Up"
|
|
||||||
action={() => movePin(channelId, -1)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{canMove && snapshotArray[snapshotArray.length - 1] !== channelId && (
|
|
||||||
<Menu.MenuItem
|
|
||||||
id="move-pin-down"
|
|
||||||
label="Move Pin Down"
|
|
||||||
action={() => movePin(channelId, +1)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => () => {
|
|
||||||
const container = findGroupChildrenByChildId("leave-channel", children);
|
|
||||||
if (container)
|
|
||||||
container.unshift(PinMenuItem(props.channel.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserContext: NavContextMenuPatchCallback = (children, props) => () => {
|
|
||||||
const container = findGroupChildrenByChildId("close-dm", children);
|
|
||||||
if (container) {
|
|
||||||
const idx = container.findIndex(c => c?.props?.id === "close-dm");
|
|
||||||
container.splice(idx, 0, PinMenuItem(props.channel.id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function addContextMenus() {
|
|
||||||
addContextMenuPatch("gdm-context", GroupDMContext);
|
|
||||||
addContextMenuPatch("user-context", UserContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeContextMenus() {
|
|
||||||
removeContextMenuPatch("gdm-context", GroupDMContext);
|
|
||||||
removeContextMenuPatch("user-context", UserContext);
|
|
||||||
}
|
|
214
src/plugins/pinDms/data.ts
Normal file
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,118 +1,134 @@
|
||||||
/*
|
/*
|
||||||
* Vencord, a modification for Discord's desktop app
|
* Vencord, a Discord client mod
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
*
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
* 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 "./styles.css";
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
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 { Channel } from "discord-types/general";
|
||||||
|
|
||||||
import { addContextMenus, removeContextMenus } from "./contextMenus";
|
import { contextMenus } from "./components/contextMenu";
|
||||||
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings";
|
import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal";
|
||||||
|
import { DEFAULT_CHUNK_SIZE } from "./constants";
|
||||||
|
import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data";
|
||||||
|
|
||||||
|
interface ChannelComponentProps {
|
||||||
|
children: React.ReactNode,
|
||||||
|
channel: Channel,
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
|
||||||
|
|
||||||
|
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
||||||
|
|
||||||
|
export let instance: any;
|
||||||
|
export const forceUpdate = () => instance?.props?._forceUpdate?.();
|
||||||
|
|
||||||
|
export const enum PinOrder {
|
||||||
|
LastMessage,
|
||||||
|
Custom
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settings = definePluginSettings({
|
||||||
|
pinOrder: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Which order should pinned DMs be displayed in?",
|
||||||
|
options: [
|
||||||
|
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
|
||||||
|
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
|
||||||
|
],
|
||||||
|
onChange: () => forceUpdate()
|
||||||
|
},
|
||||||
|
|
||||||
|
dmSectioncollapsed: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Collapse DM sections",
|
||||||
|
default: false,
|
||||||
|
onChange: () => forceUpdate()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PinDMs",
|
name: "PinDMs",
|
||||||
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
|
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
|
||||||
authors: [Devs.Ven, Devs.Strencher],
|
authors: [Devs.Ven, Devs.Aria],
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
|
contextMenus,
|
||||||
start: addContextMenus,
|
|
||||||
stop: removeContextMenus,
|
|
||||||
|
|
||||||
usePinCount(channelIds: string[]) {
|
|
||||||
const pinnedDms = usePinnedDms();
|
|
||||||
// See comment on 2nd patch for reasoning
|
|
||||||
return channelIds.length ? [pinnedDms.size] : [];
|
|
||||||
},
|
|
||||||
|
|
||||||
getChannel(channels: Record<string, Channel>, idx: number) {
|
|
||||||
return channels[getPinAt(idx)];
|
|
||||||
},
|
|
||||||
|
|
||||||
isPinned,
|
|
||||||
getSnapshot: sortedSnapshot,
|
|
||||||
|
|
||||||
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
|
|
||||||
if (!isPinned(channelId))
|
|
||||||
return (
|
|
||||||
(rowHeight + padding) * 2 // header
|
|
||||||
+ rowHeight * snapshotArray.length // pins
|
|
||||||
+ originalOffset // original pin offset minus pins
|
|
||||||
);
|
|
||||||
|
|
||||||
return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding;
|
|
||||||
},
|
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
// Patch DM list
|
|
||||||
{
|
{
|
||||||
find: ".privateChannelsHeaderContainer,",
|
find: ".privateChannelsHeaderContainer,",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// filter Discord's privateChannelIds list to remove pins, and pass
|
// Filter out pinned channels from the private channel list
|
||||||
// pinCount as prop. This needs to be here so that the entire DM list receives
|
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/,
|
||||||
// updates on pin/unpin
|
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))"
|
||||||
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i),/,
|
|
||||||
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1),"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// sections is an array of numbers, where each element is a section and
|
// Insert the pinned channels to sections
|
||||||
// the number is the amount of rows. Add our pinCount in second place
|
match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/,
|
||||||
// - Section 1: buttons for pages like Friends & Library
|
replace: "...$self.makeProps(this,{$&})"
|
||||||
// - Section 2: our pinned dms
|
},
|
||||||
// - Section 3: the normal dm list
|
|
||||||
match: /(?<=renderRow:this\.renderRow,)sections:\[\i,/,
|
// Rendering
|
||||||
// For some reason, adding our sections when no private channels are ready yet
|
{
|
||||||
// makes DMs infinitely load. Thus usePinCount returns either a single element
|
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
|
||||||
// array with the count, or an empty array. Due to spreading, only in the former
|
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);"
|
||||||
// case will an element be added to the outer array
|
|
||||||
// Thanks for the fix, Strencher!
|
|
||||||
replace: "$&...this.props.pinCount??[],"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages"
|
match: /"renderSection",(\i)=>{/,
|
||||||
// lookbehind is used to lookup parameter name. We could use arguments[0], but
|
replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);"
|
||||||
// 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"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Patch channel lookup inside renderDM
|
match: /(?<=span",{)className:\i\.headerText,/,
|
||||||
// channel=channels[channelIds[row]];
|
replace: "...$self.makeSpanProps(),$&"
|
||||||
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]];
|
// Fix Row Height
|
||||||
replace: "$1===1?$self.getChannel($4,$2):$3;"
|
{
|
||||||
|
match: /(?<="getRowHeight",.{1,100}return 1===)\i/,
|
||||||
|
replace: "($&-$self.categoryLen())"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Fix getRowHeight's check for whether this is the DMs section
|
match: /"getRowHeight",\((\i),(\i)\)=>{/,
|
||||||
// DMS (inlined) === section
|
replace: "$&if($self.isChannelHidden($1,$2))return 0;"
|
||||||
match: /(?<=getRowHeight=\(.{2,50}?)1===\i/,
|
|
||||||
// DMS (inlined) === section - 1
|
|
||||||
replace: "$&-1"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Fix ScrollTo
|
||||||
{
|
{
|
||||||
// Override scrollToChannel to properly account for pinned channels
|
// Override scrollToChannel to properly account for pinned channels
|
||||||
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
|
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
|
||||||
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
|
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
|
// Fix Alt Up/Down navigation
|
||||||
{
|
{
|
||||||
find: ".Routes.APPLICATION_STORE&&",
|
find: ".Routes.APPLICATION_STORE&&",
|
||||||
|
@ -120,16 +136,225 @@ export default definePlugin({
|
||||||
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
|
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
|
||||||
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
|
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
|
||||||
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
|
// ....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
|
// fix alt+shift+up/down
|
||||||
{
|
{
|
||||||
find: ".getFlattenedGuildIds()],",
|
find: ".getFlattenedGuildIds()],",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
|
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
|
||||||
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
|
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
|
sections: null as number[] | null,
|
||||||
|
|
||||||
|
set _instance(i: any) {
|
||||||
|
this.instance = i;
|
||||||
|
instance = i;
|
||||||
|
},
|
||||||
|
|
||||||
|
startAt: StartAt.WebpackReady,
|
||||||
|
start: init,
|
||||||
|
flux: {
|
||||||
|
CONNECTION_OPEN: init,
|
||||||
|
},
|
||||||
|
|
||||||
|
isPinned,
|
||||||
|
categoryLen,
|
||||||
|
getSections,
|
||||||
|
getAllUncollapsedChannels,
|
||||||
|
requireSettingsMenu,
|
||||||
|
|
||||||
|
makeProps(instance, { sections }: { sections: number[]; }) {
|
||||||
|
this._instance = instance;
|
||||||
|
this.sections = sections;
|
||||||
|
|
||||||
|
this.sections.splice(1, 0, ...this.getSections());
|
||||||
|
|
||||||
|
if (this.instance?.props?.privateChannelIds?.length === 0) {
|
||||||
|
// dont render direct messages header
|
||||||
|
this.sections[this.sections.length - 1] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sections: this.sections,
|
||||||
|
chunkSize: this.getChunkSize(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
makeSpanProps() {
|
||||||
|
return {
|
||||||
|
onClick: () => this.collapseDMList(),
|
||||||
|
role: "button",
|
||||||
|
style: { cursor: "pointer" }
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getChunkSize() {
|
||||||
|
// the chunk size is the amount of rows (measured in pixels) that are rendered at once (probably)
|
||||||
|
// the higher the chunk size, the more rows are rendered at once
|
||||||
|
// also if the chunk size is 0 it will render everything at once
|
||||||
|
|
||||||
|
const sections = this.getSections();
|
||||||
|
const sectionHeaderSizePx = sections.length * 40;
|
||||||
|
// (header heights + DM heights + DEFAULT_CHUNK_SIZE) * 1.5
|
||||||
|
// we multiply everything by 1.5 so it only gets unmounted after the entire list is off screen
|
||||||
|
return (sectionHeaderSizePx + sections.reduce((acc, v) => acc += v + 44, 0) + DEFAULT_CHUNK_SIZE) * 1.5;
|
||||||
|
},
|
||||||
|
|
||||||
|
isCategoryIndex(sectionIndex: number) {
|
||||||
|
return this.sections && sectionIndex > 0 && sectionIndex < this.sections.length - 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
isChannelIndex(sectionIndex: number, channelIndex: number) {
|
||||||
|
if (settings.store.dmSectioncollapsed && sectionIndex !== 0)
|
||||||
|
return true;
|
||||||
|
const cat = categories[sectionIndex - 1];
|
||||||
|
return this.isCategoryIndex(sectionIndex) && (cat?.channels?.length === 0 || cat?.channels[channelIndex]);
|
||||||
|
},
|
||||||
|
|
||||||
|
isDMSectioncollapsed() {
|
||||||
|
return settings.store.dmSectioncollapsed;
|
||||||
|
},
|
||||||
|
|
||||||
|
collapseDMList() {
|
||||||
|
settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed;
|
||||||
|
forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
isChannelHidden(categoryIndex: number, channelIndex: number) {
|
||||||
|
if (categoryIndex === 0) return false;
|
||||||
|
|
||||||
|
if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;
|
||||||
|
|
||||||
|
const category = categories[categoryIndex - 1];
|
||||||
|
if (!category) return false;
|
||||||
|
|
||||||
|
return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex];
|
||||||
|
},
|
||||||
|
|
||||||
|
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
|
||||||
|
if (!isPinned(channelId))
|
||||||
|
return (
|
||||||
|
(rowHeight + padding) * 2 // header
|
||||||
|
+ rowHeight * this.getAllUncollapsedChannels().length // pins
|
||||||
|
+ originalOffset // original pin offset minus pins
|
||||||
|
);
|
||||||
|
|
||||||
|
return rowHeight * (this.getAllUncollapsedChannels().indexOf(channelId) + preRenderedChildren) + padding;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => {
|
||||||
|
const category = categories[section - 1];
|
||||||
|
|
||||||
|
if (!category) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
|
||||||
|
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
|
||||||
|
onClick={async () => {
|
||||||
|
await collapseCategory(category.id, !category.collapsed);
|
||||||
|
forceUpdate();
|
||||||
|
}}
|
||||||
|
onContextMenu={e => {
|
||||||
|
ContextMenuApi.openContextMenu(e, () => (
|
||||||
|
<Menu.Menu
|
||||||
|
navId="vc-pindms-header-menu"
|
||||||
|
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||||
|
color="danger"
|
||||||
|
aria-label="Pin DMs Category Menu"
|
||||||
|
>
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-pindms-edit-category"
|
||||||
|
label="Edit Category"
|
||||||
|
action={() => openCategoryModal(category.id, null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
canMoveCategory(category.id) && (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
|
||||||
|
id="vc-pindms-move-category-up"
|
||||||
|
label="Move Up"
|
||||||
|
action={() => moveCategory(category.id, -1).then(() => forceUpdate())}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
|
||||||
|
id="vc-pindms-move-category-down"
|
||||||
|
label="Move Down"
|
||||||
|
action={() => moveCategory(category.id, 1).then(() => forceUpdate())}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<Menu.MenuSeparator />
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-pindms-delete-category"
|
||||||
|
color="danger"
|
||||||
|
label="Delete Category"
|
||||||
|
action={() => removeCategory(category.id).then(() => forceUpdate())}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</Menu.Menu>
|
||||||
|
));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={headerClasses.headerText}>
|
||||||
|
{category?.name ?? "uh oh"}
|
||||||
|
</span>
|
||||||
|
<svg className="vc-pindms-collapse-icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
|
||||||
|
</svg>
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
|
||||||
|
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
|
||||||
|
|
||||||
|
if (!channel || !category) return null;
|
||||||
|
if (this.isChannelHidden(sectionIndex, index)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChannelComponent
|
||||||
|
channel={channel}
|
||||||
|
selected={this.instance.props.selectedChannelId === channel.id}
|
||||||
|
>
|
||||||
|
{channel.id}
|
||||||
|
</ChannelComponent>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
||||||
|
const category = categories[sectionIndex - 1];
|
||||||
|
if (!category) return { channel: null, category: null };
|
||||||
|
|
||||||
|
const channelId = this.getCategoryChannels(category)[index];
|
||||||
|
|
||||||
|
return { channel: channels[channelId], category };
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategoryChannels(category: Category) {
|
||||||
|
if (category.channels.length === 0) return [];
|
||||||
|
|
||||||
|
if (settings.store.pinOrder === PinOrder.LastMessage) {
|
||||||
|
return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category.channels.includes(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
return category?.channels ?? [];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 { Settings } from "@api/Settings";
|
||||||
import { VENCORD_USER_AGENT } from "@utils/constants";
|
import { debounce } from "@shared/debounce";
|
||||||
import { debounce } from "@utils/debounce";
|
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||||
import { getCurrentChannel } from "@utils/discord";
|
import { getCurrentChannel } from "@utils/discord";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { UserProfileStore, UserStore } from "@webpack/common";
|
import { UserProfileStore, UserStore } from "@webpack/common";
|
||||||
|
|
|
@ -54,7 +54,7 @@ const settings = definePluginSettings({
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "QuickReply",
|
name: "QuickReply",
|
||||||
authors: [Devs.obscurity, Devs.Ven, Devs.pylix],
|
authors: [Devs.fawn, Devs.Ven, Devs.pylix],
|
||||||
description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds",
|
description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds",
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
|
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
@ -49,9 +51,11 @@ const ReadAllButton = () => (
|
||||||
<Button
|
<Button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
size={Button.Sizes.MIN}
|
size={Button.Sizes.MIN}
|
||||||
color={Button.Colors.BRAND}
|
color={Button.Colors.CUSTOM}
|
||||||
style={{ marginTop: "2px", marginBottom: "8px", marginLeft: "9px" }}
|
className="vc-ranb-button"
|
||||||
>Read all</Button>
|
>
|
||||||
|
Read All
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
|
|
11
src/plugins/readAllNotificationsButton/style.css
Normal file
11
src/plugins/readAllNotificationsButton/style.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.vc-ranb-button {
|
||||||
|
color: var(--interactive-normal);
|
||||||
|
padding: 0 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-ranb-button:hover {
|
||||||
|
color: var(--interactive-active);
|
||||||
|
}
|
5
src/plugins/resurrectHome/README.md
Normal file
5
src/plugins/resurrectHome/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# ResurrectHome
|
||||||
|
|
||||||
|
Brings back the phased out [Server Home](https://support.discord.com/hc/en-us/articles/6156116949911-Server-Home-Beta) feature!
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/61953774/98d5d667-bbb9-48b8-872d-c9b3980f6506)
|
119
src/plugins/resurrectHome/index.tsx
Normal file
119
src/plugins/resurrectHome/index.tsx
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { findGroupChildrenByChildId } from "@api/ContextMenu";
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { Menu } from "@webpack/common";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
forceServerHome: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Force the Server Guide to be the Server Home tab when it is enabled.",
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function useForceServerHome() {
|
||||||
|
const { forceServerHome } = settings.use(["forceServerHome"]);
|
||||||
|
|
||||||
|
return forceServerHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ResurrectHome",
|
||||||
|
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.",
|
||||||
|
authors: [Devs.Dolfies, Devs.Nuckyz],
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
// Force home deprecation override
|
||||||
|
{
|
||||||
|
find: "GuildFeatures.GUILD_HOME_DEPRECATION_OVERRIDE",
|
||||||
|
all: true,
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /\i\.hasFeature\(\i\.GuildFeatures\.GUILD_HOME_DEPRECATION_OVERRIDE\)/g,
|
||||||
|
replace: "true"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Disable feedback prompts
|
||||||
|
{
|
||||||
|
find: "GuildHomeFeedbackExperiment.definition.id",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /return{showFeedback:.+?,setOnDismissedFeedback:(\i)}/,
|
||||||
|
replace: "return{showFeedback:false,setOnDismissedFeedback:$1}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// This feature was never finished, so the patch is disabled
|
||||||
|
|
||||||
|
// Enable guild feed render mode selector
|
||||||
|
// {
|
||||||
|
// find: "2022-01_home_feed_toggle",
|
||||||
|
// replacement: [
|
||||||
|
// {
|
||||||
|
// match: /showSelector:!1/,
|
||||||
|
// replace: "showSelector:true"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
|
||||||
|
// Fix focusMessage clearing previously cached messages and causing a loop when fetching messages around home messages
|
||||||
|
{
|
||||||
|
find: '"MessageActionCreators"',
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/,
|
||||||
|
replace: "before:$1,"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Force Server Home instead of Server Guide
|
||||||
|
{
|
||||||
|
find: "61eef9_2",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=getMutableGuildChannelsForGuild\(\i\)\);)(?=if\(null==\i\|\|)/,
|
||||||
|
replace: "if($self.useForceServerHome())return false;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
useForceServerHome,
|
||||||
|
|
||||||
|
contextMenus: {
|
||||||
|
"guild-context"(children, props) {
|
||||||
|
const forceServerHome = useForceServerHome();
|
||||||
|
|
||||||
|
if (!props?.guild) return;
|
||||||
|
|
||||||
|
const group = findGroupChildrenByChildId("hide-muted-channels", children);
|
||||||
|
|
||||||
|
group?.unshift(
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
key="force-server-home"
|
||||||
|
id="force-server-home"
|
||||||
|
label="Force Server Home"
|
||||||
|
checked={forceServerHome}
|
||||||
|
action={() => settings.store.forceServerHome = !forceServerHome}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -30,9 +30,9 @@ export default definePlugin({
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".removeObscurity=",
|
find: ".removeObscurity,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\.removeObscurity=(\i)=>{)/,
|
match: /(?<="removeObscurity",(\i)=>{)/,
|
||||||
replace: (_, event) => `$self.reveal(${event});`
|
replace: (_, event) => `$self.reveal(${event});`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { OpenExternalIcon } from "@components/Icons";
|
import { OpenExternalIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
@ -84,7 +84,7 @@ function makeSearchItem(src: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
|
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
||||||
if (props?.reverseImageSearchType !== "img") return;
|
if (props?.reverseImageSearchType !== "img") return;
|
||||||
|
|
||||||
const src = props.itemHref ?? props.itemSrc;
|
const src = props.itemHref ?? props.itemSrc;
|
||||||
|
@ -93,7 +93,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
|
||||||
group?.push(makeSearchItem(src));
|
group?.push(makeSearchItem(src));
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
|
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
||||||
if (!props?.src) return;
|
if (!props?.src) return;
|
||||||
|
|
||||||
const group = findGroupChildrenByChildId("copy-native-link", children) ?? children;
|
const group = findGroupChildrenByChildId("copy-native-link", children) ?? children;
|
||||||
|
@ -115,14 +115,8 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
contextMenus: {
|
||||||
start() {
|
"message": messageContextMenuPatch,
|
||||||
addContextMenuPatch("message", messageContextMenuPatch);
|
"image-context": imageContextMenuPatch
|
||||||
addContextMenuPatch("image-context", imageContextMenuPatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
removeContextMenuPatch("image-context", imageContextMenuPatch);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
|
import { useAwaiter, useForceUpdater } from "@utils/react";
|
||||||
import { find, findByPropsLazy } from "@webpack";
|
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
|
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { Auth, authorize } from "../auth";
|
import { Auth, authorize } from "../auth";
|
||||||
|
@ -31,7 +31,7 @@ import ReviewComponent from "./ReviewComponent";
|
||||||
const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms");
|
const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms");
|
||||||
const { ChatInputTypes } = findByPropsLazy("ChatInputTypes");
|
const { ChatInputTypes } = findByPropsLazy("ChatInputTypes");
|
||||||
|
|
||||||
const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default);
|
const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA", "input");
|
||||||
const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer");
|
const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer");
|
||||||
|
|
||||||
interface UserProps {
|
interface UserProps {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import ExpandableHeader from "@components/ExpandableHeader";
|
import ExpandableHeader from "@components/ExpandableHeader";
|
||||||
import { OpenExternalIcon } from "@components/Icons";
|
import { OpenExternalIcon } from "@components/Icons";
|
||||||
|
@ -36,7 +36,7 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { showToast } from "./utils";
|
import { showToast } from "./utils";
|
||||||
|
|
||||||
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
|
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => {
|
||||||
children.push(
|
children.push(
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
label="View Reviews"
|
label="View Reviews"
|
||||||
|
@ -53,6 +53,9 @@ export default definePlugin({
|
||||||
authors: [Devs.mantikafasi, Devs.Ven],
|
authors: [Devs.mantikafasi, Devs.Ven],
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
|
contextMenus: {
|
||||||
|
"guild-header-popout": guildPopoutPatch
|
||||||
|
},
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
@ -69,8 +72,6 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
|
|
||||||
|
|
||||||
const s = settings.store;
|
const s = settings.store;
|
||||||
const { lastReviewId, notifyReviews } = s;
|
const { lastReviewId, notifyReviews } = s;
|
||||||
|
|
||||||
|
@ -127,10 +128,6 @@ export default definePlugin({
|
||||||
}, 4000);
|
}, 4000);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("guild-header-popout", guildPopoutPatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
getReviewsComponent: ErrorBoundary.wrap((user: User) => {
|
getReviewsComponent: ErrorBoundary.wrap((user: User) => {
|
||||||
const [reviewCount, setReviewCount] = useState<number>();
|
const [reviewCount, setReviewCount] = useState<number>();
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
|
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
|
||||||
|
@ -112,9 +113,8 @@ export default definePlugin({
|
||||||
return colorString && parseInt(colorString.slice(1), 16);
|
return colorString && parseInt(colorString.slice(1), 16);
|
||||||
},
|
},
|
||||||
|
|
||||||
roleGroupColor({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) {
|
roleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => {
|
||||||
const guild = GuildStore.getGuild(guildId);
|
const role = GuildStore.getRole(guildId, id);
|
||||||
const role = guild?.roles[id];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span style={{
|
<span style={{
|
||||||
|
@ -125,7 +125,7 @@ export default definePlugin({
|
||||||
{title ?? label} — {count}
|
{title ?? label} — {count}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
}, { noop: true }),
|
||||||
|
|
||||||
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {
|
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { ReplyIcon } from "@components/Icons";
|
import { ReplyIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
@ -27,7 +27,7 @@ import { Message } from "discord-types/general";
|
||||||
|
|
||||||
const messageUtils = findByPropsLazy("replyToMessage");
|
const messageUtils = findByPropsLazy("replyToMessage");
|
||||||
|
|
||||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => {
|
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {
|
||||||
// make sure the message is in the selected channel
|
// make sure the message is in the selected channel
|
||||||
if (SelectedChannelStore.getChannelId() !== message.channel_id) return;
|
if (SelectedChannelStore.getChannelId() !== message.channel_id) return;
|
||||||
const channel = ChannelStore.getChannel(message?.channel_id);
|
const channel = ChannelStore.getChannel(message?.channel_id);
|
||||||
|
@ -38,7 +38,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
|
||||||
const dmGroup = findGroupChildrenByChildId("pin", children);
|
const dmGroup = findGroupChildrenByChildId("pin", children);
|
||||||
if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) {
|
if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) {
|
||||||
const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin");
|
const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin");
|
||||||
return dmGroup.splice(pinIndex + 1, 0, (
|
dmGroup.splice(pinIndex + 1, 0, (
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
id="reply"
|
id="reply"
|
||||||
label={i18n.Messages.MESSAGE_ACTION_REPLY}
|
label={i18n.Messages.MESSAGE_ACTION_REPLY}
|
||||||
|
@ -46,12 +46,13 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
|
||||||
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
|
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// servers
|
// servers
|
||||||
const serverGroup = findGroupChildrenByChildId("mark-unread", children);
|
const serverGroup = findGroupChildrenByChildId("mark-unread", children);
|
||||||
if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) {
|
if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) {
|
||||||
return serverGroup.unshift((
|
serverGroup.unshift((
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
id="reply"
|
id="reply"
|
||||||
label={i18n.Messages.MESSAGE_ACTION_REPLY}
|
label={i18n.Messages.MESSAGE_ACTION_REPLY}
|
||||||
|
@ -59,6 +60,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
|
||||||
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
|
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,12 +69,7 @@ export default definePlugin({
|
||||||
name: "SearchReply",
|
name: "SearchReply",
|
||||||
description: "Adds a reply button to search results",
|
description: "Adds a reply button to search results",
|
||||||
authors: [Devs.Aria],
|
authors: [Devs.Aria],
|
||||||
|
contextMenus: {
|
||||||
start() {
|
"message": messageContextMenuPatch
|
||||||
addContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: "call_ringing_beat\"",
|
find: "call_ringing_beat\"",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /500===\i\.random\(1,1e3\)/,
|
match: /500===\i\(\)\.random\(1,1e3\)/,
|
||||||
replace: "true"
|
replace: "true"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { classes } from "@utils/misc";
|
||||||
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { findByPropsLazy, findExportedComponentLazy } from "@webpack";
|
import { findByPropsLazy, findExportedComponentLazy } from "@webpack";
|
||||||
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
||||||
import { Guild, User } from "discord-types/general";
|
import { Guild, User } from "discord-types/general";
|
||||||
|
|
||||||
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
||||||
|
@ -172,7 +172,7 @@ function ServerInfoTab({ guild }: GuildProps) {
|
||||||
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
|
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
|
||||||
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
|
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
|
||||||
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
|
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
|
||||||
"Roles": Object.keys(guild.roles).length - 1, // - @everyone
|
"Roles": Object.keys(GuildStore.getRoles(guild.id)).length - 1, // - @everyone
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Menu } from "@webpack/common";
|
import { Menu } from "@webpack/common";
|
||||||
|
@ -12,7 +12,7 @@ import { Guild } from "discord-types/general";
|
||||||
|
|
||||||
import { openGuildProfileModal } from "./GuildProfileModal";
|
import { openGuildProfileModal } from "./GuildProfileModal";
|
||||||
|
|
||||||
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => {
|
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => {
|
||||||
const group = findGroupChildrenByChildId("privacy", children);
|
const group = findGroupChildrenByChildId("privacy", children);
|
||||||
|
|
||||||
group?.push(
|
group?.push(
|
||||||
|
@ -29,12 +29,8 @@ export default definePlugin({
|
||||||
description: "Allows you to view info about a server by right clicking it in the server list",
|
description: "Allows you to view info about a server by right clicking it in the server list",
|
||||||
authors: [Devs.Ven, Devs.Nuckyz],
|
authors: [Devs.Ven, Devs.Nuckyz],
|
||||||
tags: ["guild", "info"],
|
tags: ["guild", "info"],
|
||||||
|
contextMenus: {
|
||||||
start() {
|
"guild-context": Patch,
|
||||||
addContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
|
"guild-header-popout": Patch
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,8 +4,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-gp-banner {
|
.vc-gp-banner {
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
aspect-ratio: auto 240 / 135;
|
||||||
|
height: 334px;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
overflow: clip;
|
||||||
|
overflow-clip-margin: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-gp-header {
|
.vc-gp-header {
|
||||||
|
|
|
@ -138,7 +138,7 @@ export default definePlugin({
|
||||||
all: true,
|
all: true,
|
||||||
// Render null instead of the buttons if the channel is hidden
|
// Render null instead of the buttons if the channel is hidden
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=renderOpenChatButton=\(\)=>{)/,
|
match: /(?<="renderOpenChatButton",\(\)=>{)/,
|
||||||
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
|
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -191,10 +191,10 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Hide the new version of unreads box for hidden channels
|
// Hide the new version of unreads box for hidden channels
|
||||||
find: '.displayName="ChannelListUnreadsStore"',
|
find: '="ChannelListUnreadsStore",',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=if\(null==(\i))(?=.{0,160}?getHasImportantUnread\)\(\i\))/g, // Global because Discord has multiple methods like that in the same module
|
match: /(?=&&\(0,\i\.getHasImportantUnread\)\((\i)\))/g, // Global because Discord has multiple methods like that in the same module
|
||||||
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
|
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -218,19 +218,19 @@ export default definePlugin({
|
||||||
find: "Missing channel in Channel.renderHeaderToolbar",
|
find: "Missing channel in Channel.renderHeaderToolbar",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=renderHeaderToolbar=\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
|
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
|
||||||
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
|
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=renderHeaderToolbar=\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
|
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
|
||||||
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
|
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /renderMobileToolbar=\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/,
|
match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/,
|
||||||
replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;`
|
replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=renderHeaderBar=\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/,
|
match: /(?<="renderHeaderBar",\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/,
|
||||||
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
|
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -305,27 +305,27 @@ export default definePlugin({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".avatars),children",
|
find: '+1]})},"overflow"))',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Create a variable for the channel prop
|
// Create a variable for the channel prop
|
||||||
match: /maxUsers:\i,users:\i.+?=(\i).+?;/,
|
match: /maxUsers:\i,users:\i.+?}=(\i).*?;/,
|
||||||
replace: (m, props) => `${m}let{shcChannel}=${props};`
|
replace: (m, props) => `${m}let{shcChannel}=${props};`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
|
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
|
||||||
match: /\i>0(?=&&.{0,60}renderPopout)/,
|
match: /\i>0(?=&&.{0,60}renderPopout)/,
|
||||||
replace: m => `($self.isHiddenChannel(shcChannel,true)?true:${m})`
|
replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
// Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
||||||
match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
|
match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
|
||||||
replace: (_, amount) => `($self.isHiddenChannel(shcChannel,true)&&${amount}<=0?0:1)`
|
replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?0:1)`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
// Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
||||||
match: /(?<="\+",)(\i)\+1/,
|
match: /(?<="\+",)(\i)\+1/,
|
||||||
replace: (m, amount) => `$self.isHiddenChannel(shcChannel,true)&&${amount}<=0?"":${m}`
|
replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${m}`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -442,7 +442,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '.displayName="GuildChannelStore"',
|
find: '="GuildChannelStore",',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Make GuildChannelStore contain hidden channels
|
// Make GuildChannelStore contain hidden channels
|
||||||
|
@ -465,7 +465,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '.displayName="NowPlayingViewStore"',
|
find: '="NowPlayingViewStore",',
|
||||||
replacement: {
|
replacement: {
|
||||||
// Make active now voice states on hidden channels
|
// Make active now voice states on hidden channels
|
||||||
match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/,
|
match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/,
|
||||||
|
|
|
@ -21,7 +21,7 @@ import "./spotifyStyles.css";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
|
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { openImageModal } from "@utils/discord";
|
import { openImageModal } from "@utils/discord";
|
||||||
import { classes, copyWithToast } from "@utils/misc";
|
import { classes, copyWithToast } from "@utils/misc";
|
||||||
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '.displayName="SpotifyStore"',
|
find: '"displayName","SpotifyStore")',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
predicate: () => settings.store.noSpotifyAutoPause,
|
predicate: () => settings.store.noSpotifyAutoPause,
|
||||||
|
|
|
@ -22,7 +22,7 @@ import definePlugin from "@utils/types";
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "TimeBarAllActivities",
|
name: "TimeBarAllActivities",
|
||||||
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
|
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
|
||||||
authors: [Devs.obscurity],
|
authors: [Devs.fawn],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "}renderTimeBar(",
|
find: "}renderTimeBar(",
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
||||||
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
|
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
|
||||||
import { addButton, removeButton } from "@api/MessagePopover";
|
import { addButton, removeButton } from "@api/MessagePopover";
|
||||||
|
@ -32,7 +32,7 @@ import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
|
||||||
import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
|
import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
|
||||||
import { translate } from "./utils";
|
import { translate } from "./utils";
|
||||||
|
|
||||||
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => () => {
|
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => {
|
||||||
if (!message.content) return;
|
if (!message.content) return;
|
||||||
|
|
||||||
const group = findGroupChildrenByChildId("copy-text", children);
|
const group = findGroupChildrenByChildId("copy-text", children);
|
||||||
|
@ -57,13 +57,15 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
|
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
|
||||||
settings,
|
settings,
|
||||||
|
contextMenus: {
|
||||||
|
"message": messageCtxPatch
|
||||||
|
},
|
||||||
// not used, just here in case some other plugin wants it or w/e
|
// not used, just here in case some other plugin wants it or w/e
|
||||||
translate,
|
translate,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addAccessory("vc-translation", props => <TranslationAccessory message={props.message} />);
|
addAccessory("vc-translation", props => <TranslationAccessory message={props.message} />);
|
||||||
|
|
||||||
addContextMenuPatch("message", messageCtxPatch);
|
|
||||||
addChatBarButton("vc-translate", TranslateChatBarIcon);
|
addChatBarButton("vc-translate", TranslateChatBarIcon);
|
||||||
|
|
||||||
addButton("vc-translate", message => {
|
addButton("vc-translate", message => {
|
||||||
|
@ -91,7 +93,6 @@ export default definePlugin({
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
removePreSendListener(this.preSend);
|
removePreSendListener(this.preSend);
|
||||||
removeContextMenuPatch("message", messageCtxPatch);
|
|
||||||
removeChatBarButton("vc-translate");
|
removeChatBarButton("vc-translate");
|
||||||
removeButton("vc-translate");
|
removeButton("vc-translate");
|
||||||
removeAccessory("vc-translation");
|
removeAccessory("vc-translation");
|
||||||
|
|
|
@ -125,7 +125,7 @@ const settings = definePluginSettings({
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "TypingIndicator",
|
name: "TypingIndicator",
|
||||||
description: "Adds an indicator if someone is typing on a channel.",
|
description: "Adds an indicator if someone is typing on a channel.",
|
||||||
authors: [Devs.Nuckyz, Devs.obscurity],
|
authors: [Devs.Nuckyz, Devs.fawn],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { ImageInvisible, ImageVisible } from "@components/Icons";
|
import { ImageInvisible, ImageVisible } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
@ -24,7 +24,7 @@ import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@web
|
||||||
|
|
||||||
const EMBED_SUPPRESSED = 1 << 2;
|
const EMBED_SUPPRESSED = 1 << 2;
|
||||||
|
|
||||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => {
|
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => {
|
||||||
const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0;
|
const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0;
|
||||||
if (!isEmbedSuppressed && !embeds.length) return;
|
if (!isEmbedSuppressed && !embeds.length) return;
|
||||||
|
|
||||||
|
@ -56,12 +56,7 @@ export default definePlugin({
|
||||||
name: "UnsuppressEmbeds",
|
name: "UnsuppressEmbeds",
|
||||||
authors: [Devs.rad, Devs.HypedDomi],
|
authors: [Devs.rad, Devs.HypedDomi],
|
||||||
description: "Allows you to unsuppress embeds in messages",
|
description: "Allows you to unsuppress embeds in messages",
|
||||||
|
contextMenus: {
|
||||||
start() {
|
"message": messageContextMenuPatch
|
||||||
addContextMenuPatch("message", messageContextMenuPatch);
|
}
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -98,8 +98,8 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".popularApplicationCommandIds,",
|
find: ".popularApplicationCommandIds,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\(0,\i\.jsx\)\(\i\.\i,{user:\i,setNote/,
|
match: /applicationId:\i\.id}\),(?=.{0,50}setNote:\i)/,
|
||||||
replace: "$self.patchPopout(arguments[0]),$&",
|
replace: "$&$self.patchPopout(arguments[0]),",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// below username
|
// below username
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings, useSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
@ -30,6 +30,8 @@ import type { ReactNode } from "react";
|
||||||
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
|
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
|
||||||
|
|
||||||
function VencordPopout(onClose: () => void) {
|
function VencordPopout(onClose: () => void) {
|
||||||
|
const { useQuickCss } = useSettings(["useQuickCss"]);
|
||||||
|
|
||||||
const pluginEntries = [] as ReactNode[];
|
const pluginEntries = [] as ReactNode[];
|
||||||
|
|
||||||
for (const plugin of Object.values(Vencord.Plugins.plugins)) {
|
for (const plugin of Object.values(Vencord.Plugins.plugins)) {
|
||||||
|
@ -68,11 +70,10 @@ function VencordPopout(onClose: () => void) {
|
||||||
/>
|
/>
|
||||||
<Menu.MenuCheckboxItem
|
<Menu.MenuCheckboxItem
|
||||||
id="vc-toolbox-quickcss-toggle"
|
id="vc-toolbox-quickcss-toggle"
|
||||||
checked={Settings.useQuickCss}
|
checked={useQuickCss}
|
||||||
label={"Enable QuickCSS"}
|
label={"Enable QuickCSS"}
|
||||||
action={() => {
|
action={() => {
|
||||||
Settings.useQuickCss = !Settings.useQuickCss;
|
Settings.useQuickCss = !useQuickCss;
|
||||||
onClose();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { ImageIcon } from "@components/Icons";
|
import { ImageIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
@ -80,7 +80,7 @@ function openImage(url: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => {
|
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null;
|
const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null;
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => {
|
const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => {
|
||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
const { id, icon, banner } = guild;
|
const { id, icon, banner } = guild;
|
||||||
|
@ -155,14 +155,9 @@ export default definePlugin({
|
||||||
|
|
||||||
openImage,
|
openImage,
|
||||||
|
|
||||||
start() {
|
contextMenus: {
|
||||||
addContextMenuPatch("user-context", UserContext);
|
"user-context": UserContext,
|
||||||
addContextMenuPatch("guild-context", GuildContext);
|
"guild-context": GuildContext
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("user-context", UserContext);
|
|
||||||
removeContextMenuPatch("guild-context", GuildContext);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
|
@ -179,7 +174,7 @@ export default definePlugin({
|
||||||
find: ".NITRO_BANNER,",
|
find: ".NITRO_BANNER,",
|
||||||
replacement: {
|
replacement: {
|
||||||
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
|
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
|
||||||
match: /style:\{(?=backgroundImage:(\i&&\i)\?"url\("\.concat\((\i),)/,
|
match: /style:\{(?=backgroundImage:(\i)\?"url\("\.concat\((\i),)/,
|
||||||
replace:
|
replace:
|
||||||
// onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
|
// 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,'
|
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { addButton, removeButton } from "@api/MessagePopover";
|
import { addButton, removeButton } from "@api/MessagePopover";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { CodeBlock } from "@components/CodeBlock";
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
|
@ -117,8 +117,8 @@ const settings = definePluginSettings({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function MakeContextCallback(name: "Guild" | "User" | "Channel") {
|
function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback {
|
||||||
const callback: NavContextMenuPatchCallback = (children, props) => () => {
|
return (children, props) => {
|
||||||
const value = props[name.toLowerCase()];
|
const value = props[name.toLowerCase()];
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings
|
if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings
|
||||||
|
@ -141,16 +141,19 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel") {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return callback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ViewRaw",
|
name: "ViewRaw",
|
||||||
description: "Copy and view the raw content/data of any message, channel or guild",
|
description: "Copy and view the raw content/data of any message, channel or guild",
|
||||||
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
|
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
|
||||||
dependencies: ["MessagePopoverAPI"],
|
dependencies: ["MessagePopoverAPI"],
|
||||||
settings,
|
settings,
|
||||||
|
contextMenus: {
|
||||||
|
"guild-context": MakeContextCallback("Guild"),
|
||||||
|
"channel-context": MakeContextCallback("Channel"),
|
||||||
|
"user-context": MakeContextCallback("User")
|
||||||
|
},
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addButton("ViewRaw", msg => {
|
addButton("ViewRaw", msg => {
|
||||||
|
@ -187,16 +190,9 @@ export default definePlugin({
|
||||||
onContextMenu: handleContextMenu
|
onContextMenu: handleContextMenu
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
addContextMenuPatch("guild-context", MakeContextCallback("Guild"));
|
|
||||||
addContextMenuPatch("channel-context", MakeContextCallback("Channel"));
|
|
||||||
addContextMenuPatch("user-context", MakeContextCallback("User"));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
removeButton("CopyRawMessage");
|
removeButton("ViewRaw");
|
||||||
removeContextMenuPatch("guild-context", MakeContextCallback("Guild"));
|
|
||||||
removeContextMenuPatch("channel-context", MakeContextCallback("Channel"));
|
|
||||||
removeContextMenuPatch("user-context", MakeContextCallback("User"));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue