Compare commits
40 commits
a2251c9066
...
09be5b9d06
Author | SHA1 | Date | |
---|---|---|---|
09be5b9d06 | |||
|
81fb7c6322 | ||
|
f39f16d34b | ||
|
0f74817e25 | ||
|
4832a9433f | ||
|
6f05612e34 | ||
|
9efc0ff579 | ||
|
63451bad25 | ||
|
6869705673 | ||
|
a2560ede1c | ||
|
93a95b6d56 | ||
|
7b24c8ac69 | ||
|
b21b6d7e5d | ||
|
ffe6512693 | ||
|
371b5b0be8 | ||
|
074ebae334 | ||
|
6face7f8ef | ||
|
2f1e86f333 | ||
|
18d63af241 | ||
|
6ab4cf0a0b | ||
|
9980c0d04f | ||
|
4a5371a746 | ||
|
77749ed5e1 | ||
|
45aa9fbb6d | ||
|
4a2657f928 | ||
|
6578eb487e | ||
|
c080a0eaac | ||
|
3ea6a96715 | ||
|
af614465a4 | ||
|
7b248ee309 | ||
|
fd25b5f296 | ||
|
77d08c5c28 | ||
|
ea11f2244f | ||
|
96126fa39f | ||
|
9bd82943e3 | ||
|
5edc94062c | ||
|
394d2060eb | ||
|
119b628f33 | ||
|
32f2043193 | ||
|
04d2dd26c4 |
75 changed files with 1675 additions and 444 deletions
12
README.md
12
README.md
|
@ -4,7 +4,9 @@
|
|||
|
||||
The cutest Discord client mod
|
||||
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334)
|
||||
| ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) |
|
||||
|:--:|
|
||||
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -28,6 +30,14 @@ Visit https://vencord.dev/download
|
|||
|
||||
https://discord.gg/D9uwnFnqmd
|
||||
|
||||
## Sponsors
|
||||
|
||||
| **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!** |
|
||||
|:--:|
|
||||
| [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated) |
|
||||
| *generated using [github-sponsor-graph](https://github.com/Vendicated/github-sponsor-graph)* |
|
||||
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#Vendicated/Vencord&Timeline">
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
"typescript": "^5.0.4",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@8.1.1",
|
||||
"packageManager": "pnpm@8.10.2",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
*/
|
||||
|
||||
import esbuild from "esbuild";
|
||||
import { readdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
|
||||
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
|
||||
|
||||
const defines = {
|
||||
IS_STANDALONE: isStandalone,
|
||||
|
@ -43,13 +45,59 @@ const nodeCommonOpts = {
|
|||
format: "cjs",
|
||||
platform: "node",
|
||||
target: ["esnext"],
|
||||
external: ["electron", "original-fs", ...commonOpts.external],
|
||||
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
|
||||
define: defines,
|
||||
};
|
||||
|
||||
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
||||
const sourcemap = watch ? "inline" : "external";
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
const globNativesPlugin = {
|
||||
name: "glob-natives-plugin",
|
||||
setup: build => {
|
||||
const filter = /^~pluginNatives$/;
|
||||
build.onResolve({ filter }, args => {
|
||||
return {
|
||||
namespace: "import-natives",
|
||||
path: args.path
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad({ filter, namespace: "import-natives" }, async () => {
|
||||
const pluginDirs = ["plugins", "userplugins"];
|
||||
let code = "";
|
||||
let natives = "\n";
|
||||
let i = 0;
|
||||
for (const dir of pluginDirs) {
|
||||
const dirPath = join("src", dir);
|
||||
if (!await existsAsync(dirPath)) continue;
|
||||
const plugins = await readdir(dirPath);
|
||||
for (const p of plugins) {
|
||||
if (!await existsAsync(join(dirPath, p, "native.ts"))) continue;
|
||||
|
||||
const nameParts = p.split(".");
|
||||
const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);
|
||||
// pluginName.thing.desktop -> PluginName.thing
|
||||
const cleanPluginName = p[0].toUpperCase() + namePartsWithoutTarget.join(".").slice(1);
|
||||
|
||||
const mod = `p${i}`;
|
||||
code += `import * as ${mod} from "./${dir}/${p}/native";\n`;
|
||||
natives += `${JSON.stringify(cleanPluginName)}:${mod},\n`;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
code += `export default {${natives}};`;
|
||||
return {
|
||||
contents: code,
|
||||
resolveDir: "./src"
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
// Discord Desktop main & renderer & preload
|
||||
esbuild.build({
|
||||
|
@ -62,7 +110,11 @@ await Promise.all([
|
|||
...defines,
|
||||
IS_DISCORD_DESKTOP: true,
|
||||
IS_VESKTOP: false
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
...nodeCommonOpts.plugins,
|
||||
globNativesPlugin
|
||||
]
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOpts,
|
||||
|
@ -107,7 +159,11 @@ await Promise.all([
|
|||
...defines,
|
||||
IS_DISCORD_DESKTOP: false,
|
||||
IS_VESKTOP: true
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
...nodeCommonOpts.plugins,
|
||||
globNativesPlugin
|
||||
]
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOpts,
|
||||
|
|
|
@ -20,8 +20,8 @@ import "../suppressExperimentalWarnings.js";
|
|||
import "../checkNodeVersion.js";
|
||||
|
||||
import { exec, execSync } from "child_process";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { readdir, readFile } from "fs/promises";
|
||||
import { constants as FsConstants, readFileSync } from "fs";
|
||||
import { access, readdir, readFile } from "fs/promises";
|
||||
import { join, relative } from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
|
@ -47,6 +47,12 @@ export const banner = {
|
|||
|
||||
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
||||
|
||||
export function existsAsync(path) {
|
||||
return access(path, FsConstants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
|
@ -79,7 +85,7 @@ export const globPlugins = kind => ({
|
|||
let plugins = "\n";
|
||||
let i = 0;
|
||||
for (const dir of pluginDirs) {
|
||||
if (!existsSync(`./src/${dir}`)) continue;
|
||||
if (!await existsAsync(`./src/${dir}`)) continue;
|
||||
const files = await readdir(`./src/${dir}`);
|
||||
for (const file of files) {
|
||||
if (file.startsWith("_") || file.startsWith(".")) continue;
|
||||
|
|
|
@ -27,6 +27,8 @@ export { PlainSettings, Settings };
|
|||
import "./utils/quickCss";
|
||||
import "./webpack/patchWebpack";
|
||||
|
||||
import { StartAt } from "@utils/types";
|
||||
|
||||
import { get as dsGet } from "./api/DataStore";
|
||||
import { showNotification } from "./api/Notifications";
|
||||
import { PlainSettings, Settings } from "./api/Settings";
|
||||
|
@ -79,7 +81,7 @@ async function syncSettings() {
|
|||
|
||||
async function init() {
|
||||
await onceReady;
|
||||
startAllPlugins();
|
||||
startAllPlugins(StartAt.WebpackReady);
|
||||
|
||||
syncSettings();
|
||||
|
||||
|
@ -130,13 +132,17 @@ async function init() {
|
|||
}
|
||||
}
|
||||
|
||||
startAllPlugins(StartAt.Init);
|
||||
init();
|
||||
|
||||
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
startAllPlugins(StartAt.DOMContentLoaded);
|
||||
|
||||
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||
document.head.append(Object.assign(document.createElement("style"), {
|
||||
id: "vencord-native-titlebar-style",
|
||||
textContent: "[class*=titleBar]{display: none!important}"
|
||||
}));
|
||||
}, { once: true });
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { IpcRes } from "@utils/types";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { PluginIpcMappings } from "main/ipcPlugins";
|
||||
import type { UserThemeHeader } from "main/themes";
|
||||
|
||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||
|
@ -17,6 +18,16 @@ export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
|
|||
return ipcRenderer.sendSync(event, ...args) as T;
|
||||
}
|
||||
|
||||
const PluginHelpers = {} as Record<string, Record<string, (...args: any[]) => Promise<any>>>;
|
||||
const pluginIpcMap = sendSync<PluginIpcMappings>(IpcEvents.GET_PLUGIN_IPC_METHOD_MAP);
|
||||
|
||||
for (const [plugin, methods] of Object.entries(pluginIpcMap)) {
|
||||
const map = PluginHelpers[plugin] = {};
|
||||
for (const [methodName, method] of Object.entries(methods)) {
|
||||
map[methodName] = (...args: any[]) => invoke(method as IpcEvents, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
themes: {
|
||||
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
|
||||
|
@ -61,12 +72,5 @@ export default {
|
|||
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
|
||||
},
|
||||
|
||||
pluginHelpers: {
|
||||
OpenInApp: {
|
||||
resolveRedirect: (url: string) => invoke<string>(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, url),
|
||||
},
|
||||
VoiceMessages: {
|
||||
readRecording: (path: string) => invoke<Uint8Array | null>(IpcEvents.VOICE_MESSAGES_READ_RECORDING, path),
|
||||
}
|
||||
}
|
||||
pluginHelpers: PluginHelpers
|
||||
};
|
||||
|
|
|
@ -24,9 +24,8 @@ import { proxyLazy } from "@utils/lazy";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { OptionType, Plugin } from "@utils/types";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
import { Constructor } from "type-fest";
|
||||
|
@ -42,7 +41,7 @@ import {
|
|||
} from "./components";
|
||||
import { openContributorModal } from "./ContributorModal";
|
||||
|
||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||
|
||||
|
|
|
@ -17,73 +17,26 @@
|
|||
*/
|
||||
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { app, ipcMain } from "electron";
|
||||
import { readFile } from "fs/promises";
|
||||
import { request } from "https";
|
||||
import { basename, normalize } from "path";
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { getSettings } from "./ipcMain";
|
||||
import PluginNatives from "~pluginNatives";
|
||||
|
||||
// FixSpotifyEmbeds
|
||||
app.on("browser-window-created", (_, win) => {
|
||||
win.webContents.on("frame-created", (_, { frame }) => {
|
||||
frame.once("dom-ready", () => {
|
||||
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
||||
const settings = getSettings().plugins?.FixSpotifyEmbeds;
|
||||
if (!settings?.enabled) return;
|
||||
const PluginIpcMappings = {} as Record<string, Record<string, string>>;
|
||||
export type PluginIpcMappings = typeof PluginIpcMappings;
|
||||
|
||||
frame.executeJavaScript(`
|
||||
const original = Audio.prototype.play;
|
||||
Audio.prototype.play = function() {
|
||||
this.volume = ${(settings.volume / 100) || 0.1};
|
||||
return original.apply(this, arguments);
|
||||
for (const [plugin, methods] of Object.entries(PluginNatives)) {
|
||||
const entries = Object.entries(methods);
|
||||
if (!entries.length) continue;
|
||||
|
||||
const mappings = PluginIpcMappings[plugin] = {};
|
||||
|
||||
for (const [methodName, method] of entries) {
|
||||
const key = `VencordPluginNative_${plugin}_${methodName}`;
|
||||
ipcMain.handle(key, method);
|
||||
mappings[methodName] = key;
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// #region OpenInApp
|
||||
// These links don't support CORS, so this has to be native
|
||||
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||
|
||||
function getRedirect(url: string) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const req = request(new URL(url), { method: "HEAD" }, res => {
|
||||
resolve(
|
||||
res.headers.location
|
||||
? getRedirect(res.headers.location)
|
||||
: url
|
||||
);
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, async (_, url: string) => {
|
||||
if (!validRedirectUrls.test(url)) return url;
|
||||
|
||||
return getRedirect(url);
|
||||
ipcMain.on(IpcEvents.GET_PLUGIN_IPC_METHOD_MAP, e => {
|
||||
e.returnValue = PluginIpcMappings;
|
||||
});
|
||||
// #endregion
|
||||
|
||||
|
||||
// #region VoiceMessages
|
||||
ipcMain.handle(IpcEvents.VOICE_MESSAGES_READ_RECORDING, async (_, filePath: string) => {
|
||||
filePath = normalize(filePath);
|
||||
const filename = basename(filePath);
|
||||
const discordBaseDirWithTrailingSlash = normalize(app.getPath("userData") + "/");
|
||||
console.log(filename, discordBaseDirWithTrailingSlash, filePath);
|
||||
if (filename !== "recording.ogg" || !filePath.startsWith(discordBaseDirWithTrailingSlash)) return null;
|
||||
|
||||
try {
|
||||
const buf = await readFile(filePath);
|
||||
return new Uint8Array(buf.buffer);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
|
|
@ -32,6 +32,8 @@ function isNewer($new: string, old: string) {
|
|||
}
|
||||
|
||||
function patchLatest() {
|
||||
if (process.env.DISABLE_UPDATER_AUTO_PATCHING) return;
|
||||
|
||||
try {
|
||||
const currentAppPath = dirname(process.execPath);
|
||||
const currentVersion = basename(currentAppPath);
|
||||
|
|
5
src/modules.d.ts
vendored
5
src/modules.d.ts
vendored
|
@ -24,6 +24,11 @@ declare module "~plugins" {
|
|||
export default plugins;
|
||||
}
|
||||
|
||||
declare module "~pluginNatives" {
|
||||
const pluginNatives: Record<string, Record<string, (event: Electron.IpcMainInvokeEvent, ...args: unknown[]) => unknown>>;
|
||||
export default pluginNatives;
|
||||
}
|
||||
|
||||
declare module "~git-hash" {
|
||||
const hash: string;
|
||||
export default hash;
|
||||
|
|
43
src/plugins/badge.ts
Normal file
43
src/plugins/badge.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/* eslint-disable header/header */
|
||||
import { BadgePosition, ProfileBadge } from "@api/Badges";
|
||||
import { Badges } from "@api/index";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
const SHIGGY_BADGE = "https://cdn.discordapp.com/emojis/1101838344146665502.gif?size=240&quality=lossless";
|
||||
const BLOBFOXBOX_BADGE = "https://cdn.discordapp.com/emojis/1036216552736952350.webp?size=240&quality=lossless";
|
||||
|
||||
const ShiggyBadge: ProfileBadge = {
|
||||
description: "true shiggy fan",
|
||||
image: SHIGGY_BADGE,
|
||||
position: BadgePosition.START,
|
||||
props: {
|
||||
style: { transform: "scale(0.9)" }
|
||||
},
|
||||
shouldShow: ({ user }) => user.id === UserStore.getCurrentUser().id,
|
||||
link: "https://ryanccn.dev/"
|
||||
};
|
||||
const BlobfoxBoxBadge: ProfileBadge = {
|
||||
description: "blobfox",
|
||||
image: BLOBFOXBOX_BADGE,
|
||||
position: BadgePosition.START,
|
||||
props: {
|
||||
style: { transform: "scale(0.9)" }
|
||||
},
|
||||
shouldShow: ({ user }) => user.id === UserStore.getCurrentUser().id,
|
||||
link: "https://ryanccn.dev/"
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "Ryan's Extra Badges",
|
||||
description: "shiggy",
|
||||
authors: [Devs.RyanCaoDev],
|
||||
dependencies: ["BadgeAPI"],
|
||||
|
||||
|
||||
start() {
|
||||
Badges.addBadge(ShiggyBadge);
|
||||
Badges.addBadge(BlobfoxBoxBadge);
|
||||
},
|
||||
});
|
|
@ -17,8 +17,7 @@
|
|||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { find, findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { useStateFromStores } from "@webpack/common";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
|
@ -26,7 +25,7 @@ import { ExpandedGuildFolderStore, settings } from ".";
|
|||
|
||||
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
|
||||
const Animations = findByPropsLazy("a", "animated", "useTransition");
|
||||
const GuildsBar = LazyComponent(() => find(m => m.type?.toString().includes('("guildsnav")')));
|
||||
const GuildsBar = findComponentByCodeLazy('("guildsnav")');
|
||||
|
||||
export default ErrorBoundary.wrap(guildsBarProps => {
|
||||
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
|
||||
|
|
|
@ -18,9 +18,8 @@
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByProps, findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher, i18n } from "@webpack/common";
|
||||
|
||||
import FolderSideBar from "./FolderSideBar";
|
||||
|
@ -31,7 +30,7 @@ enum FolderIconDisplay {
|
|||
MoreThanOneFolderExpanded
|
||||
}
|
||||
|
||||
const GuildsTree = proxyLazy(() => findByProps("GuildsTree").GuildsTree);
|
||||
const { GuildsTree } = findByPropsLazy("GuildsTree");
|
||||
const SortedGuildStore = findStoreLazy("SortedGuildStore");
|
||||
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
|
||||
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
|
||||
|
|
63
src/plugins/bottom/components/Indicator.tsx
Normal file
63
src/plugins/bottom/components/Indicator.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This plugin was modified from code licensed under the following license:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021-present Sebastian Law
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Tooltip } from "@webpack/common";
|
||||
|
||||
export default function Indicator({ layers, bottom }: { layers: number; bottom: boolean; }) {
|
||||
return (
|
||||
<Tooltip color="black" position="top" text={layers <= 1 ? "🥺" : `Decoded from ${layers} nested bottom messages`}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<span
|
||||
className={`power-bottom-indicator ${findByPropsLazy("edited").edited}`}
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{bottom ? "(bottom)" : "(original)"}
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
73
src/plugins/bottom/encoding.ts
Normal file
73
src/plugins/bottom/encoding.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file was adapted from https://github.com/bottom-software-foundation/bottom-js
|
||||
* Which is, hopefully, licensed under MIT.
|
||||
*/
|
||||
|
||||
const CHARACTER_VALUES: [number, string][] = [
|
||||
[200, "🫂"],
|
||||
[50, "💖"],
|
||||
[10, "✨"],
|
||||
[5, "🥺"],
|
||||
[1, ","],
|
||||
[0, "❤️"],
|
||||
];
|
||||
const SECTION_SEPERATOR = "👉👈";
|
||||
const FINAL_TERMINATOR = new RegExp(`(${SECTION_SEPERATOR})?$`);
|
||||
|
||||
function encodeChar(charValue: number): string {
|
||||
if (charValue === 0) return "";
|
||||
const [val, currentCase]: [number, string] =
|
||||
CHARACTER_VALUES.find(([val]) => charValue >= val) || CHARACTER_VALUES[-1];
|
||||
return `${currentCase}${encodeChar(charValue - val)}`;
|
||||
}
|
||||
|
||||
export function encode(value: string): string {
|
||||
return Array.from(new TextEncoder().encode(value))
|
||||
.map((v: number) => encodeChar(v) + SECTION_SEPERATOR)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function decode(value: string): string {
|
||||
return new TextDecoder().decode(Uint8Array.from(
|
||||
value
|
||||
.trim()
|
||||
.replace(FINAL_TERMINATOR, "")
|
||||
.split(SECTION_SEPERATOR)
|
||||
.map(letters => {
|
||||
return Array.from(letters)
|
||||
.map(character => {
|
||||
const [value, emoji]: [number, string] = CHARACTER_VALUES.find(
|
||||
([_, em]) => em === character
|
||||
) || [-1, ""];
|
||||
if (!emoji) {
|
||||
throw new TypeError(`Invalid bottom text: '${character}'`);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.reduce((p, c) => p + c);
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
export default {
|
||||
encode: encode,
|
||||
decode: decode
|
||||
};
|
158
src/plugins/bottom/handler.ts
Normal file
158
src/plugins/bottom/handler.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This plugin was modified from code licensed under the following license:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021-present Sebastian Law
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { FluxDispatcher, MessageStore } from "@webpack/common";
|
||||
import type { Message } from "discord-types/general";
|
||||
|
||||
import Bottom from "./encoding";
|
||||
|
||||
class BottomHandler {
|
||||
|
||||
cache: Record<string, Record<string, { originalContent: string; top?: boolean; layers?: number; }>>;
|
||||
re: RegExp;
|
||||
|
||||
constructor() {
|
||||
this.cache = {};
|
||||
this.re = /((?:((?:\uD83E\uDEC2)?(?:💖)*(?:✨)*(?:🥺)*(?:,)*(❤️)?)(?:👉👈|\u200b))+)/gm;
|
||||
}
|
||||
|
||||
isTranslated(message) {
|
||||
if (
|
||||
!this.cache[message.channel_id] ||
|
||||
!this.cache[message.channel_id][message.id]
|
||||
) { return false; }
|
||||
|
||||
return this.cache[message.channel_id][message.id].originalContent !== message.content;
|
||||
}
|
||||
|
||||
translate(text: string, notNested: boolean) {
|
||||
var original = text;
|
||||
var translated = text;
|
||||
var layers = 0;
|
||||
while (original.match(this.re)) {
|
||||
translated = original.replace(this.re, (str, p1, offset, s) => Bottom.decode(p1) || p1);
|
||||
|
||||
// the regex can sometimes pick up invalid bottom in which case we want to return to avoid an infinite loop
|
||||
if (translated === original || notNested) break;
|
||||
else {
|
||||
original = translated;
|
||||
layers++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
translated: translated,
|
||||
layers: layers,
|
||||
};
|
||||
}
|
||||
|
||||
translateMessage(message: Message, decodeLayers: boolean) {
|
||||
if (!message.content || message.content.length === 0) {
|
||||
return "";
|
||||
}
|
||||
// Build cache if it doesn't exist
|
||||
if (!this.cache[message.channel_id]) {
|
||||
this.cache[message.channel_id] = {};
|
||||
}
|
||||
if (!this.cache[message.channel_id][message.id]) {
|
||||
this.cache[message.channel_id][message.id] = {
|
||||
originalContent: message.content,
|
||||
};
|
||||
}
|
||||
|
||||
const cached = this.cache[message.channel_id][message.id];
|
||||
|
||||
if (this.isTranslated(message)) {
|
||||
// if we're reverting back to original, just set the content back to original
|
||||
message.content = cached.originalContent;
|
||||
this.updateMessage(message);
|
||||
} else {
|
||||
// the message hasn't been edited, let's try to decode it
|
||||
const { translated, layers } = this.translate(message.content, !decodeLayers);
|
||||
if (translated === message.content) {
|
||||
// we don't want to do anything if there is no bottom
|
||||
// since the translation fails, mark this message to not show the indicator
|
||||
cached.top = true;
|
||||
throw new Error("No Bottom detected 🥺");
|
||||
} else {
|
||||
// let the indicator show how many layers of decoding we did
|
||||
cached.layers = layers;
|
||||
message.content = translated;
|
||||
this.updateMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateMessage(message: Message) {
|
||||
console.log({
|
||||
bottomTranslation: true,
|
||||
type: "MESSAGE_UPDATE",
|
||||
message,
|
||||
});
|
||||
FluxDispatcher.dispatch({
|
||||
bottomTranslation: true,
|
||||
type: "MESSAGE_UPDATE",
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
for (const channelID in this.cache) {
|
||||
for (const messageID in this.cache[channelID]) {
|
||||
this.removeMessage(channelID, messageID);
|
||||
}
|
||||
}
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
removeMessage(channelID: string, messageID: string, reset = true) {
|
||||
const message = MessageStore.getMessage(channelID, messageID);
|
||||
if (reset) {
|
||||
message.content = this.cache[channelID][messageID].originalContent;
|
||||
this.updateMessage(message);
|
||||
}
|
||||
delete this.cache[channelID][messageID];
|
||||
}
|
||||
}
|
||||
|
||||
export default BottomHandler;
|
262
src/plugins/bottom/index.tsx
Normal file
262
src/plugins/bottom/index.tsx
Normal file
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This plugin was modified from code licensed under the following license:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021-present Sebastian Law
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { findOption, RequiredMessageOption } from "@api/Commands";
|
||||
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
||||
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
|
||||
import { addButton, removeButton } from "@api/MessagePopover";
|
||||
import { definePluginSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore, Toasts } from "@webpack/common";
|
||||
|
||||
import Indicator from "./components/Indicator";
|
||||
import Bottom from "./encoding";
|
||||
import BottomHandler from "./handler";
|
||||
|
||||
const Handler = new BottomHandler();
|
||||
|
||||
const settings = definePluginSettings({
|
||||
"decode-layers": {
|
||||
description: "Decode Layers",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
"auto-encode-send": {
|
||||
description: "Automatically encode outgoing messages",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
"encode-send-type": {
|
||||
description: "Automatic Encode Behavior",
|
||||
type: OptionType.SELECT,
|
||||
options:
|
||||
[
|
||||
{
|
||||
label: "All",
|
||||
default: true,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: "Inline (Greedy)",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: "Inline (Parsed)",
|
||||
value: 2,
|
||||
}
|
||||
],
|
||||
},
|
||||
"inline-bottom-prefix": {
|
||||
description: "Inline bottom prefix",
|
||||
type: OptionType.STRING,
|
||||
default: "👉",
|
||||
},
|
||||
"inline-bottom-suffix": {
|
||||
description: "Inline bottom suffix",
|
||||
type: OptionType.STRING,
|
||||
default: "👈",
|
||||
},
|
||||
});
|
||||
|
||||
const escapeRegex: (string: string) => string = string => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
function count(string: string, subString: string): number {
|
||||
var n = 0;
|
||||
var pos = 0;
|
||||
const step = subString.length;
|
||||
|
||||
while (true) {
|
||||
pos = string.indexOf(subString, pos);
|
||||
if (pos >= 0) {
|
||||
n++;
|
||||
pos += step;
|
||||
} else break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function inlineEncode(p: string, s: string, text: string): string {
|
||||
var np = count(text, p);
|
||||
var ns = count(text, s);
|
||||
|
||||
if (np === 0 || ns === 0) return text;
|
||||
|
||||
var pl = p.length;
|
||||
var sl = s.length;
|
||||
const result: string[] = [];
|
||||
let idx = 0;
|
||||
|
||||
while (true) {
|
||||
var startIndex = text.indexOf(p, idx);
|
||||
|
||||
if (startIndex < 0) {
|
||||
result.push(text.slice(idx));
|
||||
break;
|
||||
}
|
||||
|
||||
var endIndex = text.indexOf(s, startIndex + pl);
|
||||
|
||||
if (endIndex < 0) {
|
||||
result.push(text.slice(idx));
|
||||
break;
|
||||
}
|
||||
|
||||
result.push(text.slice(idx, startIndex));
|
||||
startIndex += pl;
|
||||
result.push(Bottom.encode(text.slice(startIndex, endIndex)));
|
||||
endIndex += sl;
|
||||
idx = endIndex;
|
||||
}
|
||||
|
||||
return result.join("");
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "Bottom",
|
||||
description: "The Vencord plugin for bottom 🥺",
|
||||
authors: [
|
||||
{
|
||||
id: 1038096782963507210n,
|
||||
name: "skyevg",
|
||||
},
|
||||
],
|
||||
dependencies: ["MessagePopoverAPI", "CommandsAPI", "MessageEventsAPI", "MessageAccessoriesAPI"],
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
addButton("bottom", msg => {
|
||||
return {
|
||||
label: "Translate Bottom",
|
||||
icon: () => (
|
||||
<svg x="0" y="0" aria-hidden="false" width="22" height="22" viewBox="0 0 36 36" fill="currentColor" className="icon">
|
||||
<circle fill="#FFCC4D" cx="18" cy="18" r="18" />
|
||||
<path fill="#65471B" d="M20.996 27c-.103 0-.206-.016-.309-.049-1.76-.571-3.615-.571-5.375 0-.524.169-1.089-.117-1.26-.642-.171-.525.117-1.089.643-1.26 2.162-.702 4.447-.702 6.609 0 .525.171.813.735.643 1.26-.137.421-.529.691-.951.691z" />
|
||||
<path fill="#FFF" d="M30.335 12.068c-.903 2.745-3.485 4.715-6.494 4.715-.144 0-.289-.005-.435-.014-1.477-.093-2.842-.655-3.95-1.584.036.495.076.997.136 1.54.152 1.388.884 2.482 2.116 3.163.82.454 1.8.688 2.813.752 1.734.109 3.57-.28 4.873-.909 1.377-.665 2.272-1.862 2.456-3.285.183-1.415-.354-2.924-1.515-4.378z" />
|
||||
<path fill="#65471B" d="M21.351 7.583c-1.297.55-1.947 2.301-1.977 5.289l.039.068c.897 1.319 2.373 2.224 4.088 2.332.114.007.228.011.341.011 2.634 0 4.849-1.937 5.253-4.524-.115-.105-.221-.212-.343-.316-3.715-3.17-6.467-3.257-7.401-2.86z" />
|
||||
<path fill="#F4900C" d="M23.841 16.783c3.009 0 5.591-1.97 6.494-4.715-.354-.443-.771-.88-1.241-1.309-.404 2.587-2.619 4.524-5.253 4.524-.113 0-.227-.004-.341-.011-1.715-.108-3.191-1.013-4.088-2.332l-.039-.068c-.007.701.021 1.473.083 2.313 1.108.929 2.473 1.491 3.95 1.584.146.01.291.014.435.014z" />
|
||||
<circle fill="#FFF" cx="21.413" cy="10.705" r="1.107" />
|
||||
<path fill="#FFF" d="M12.159 16.783c-3.009 0-5.591-1.97-6.494-4.715-1.161 1.454-1.697 2.963-1.515 4.377.185 1.423 1.079 2.621 2.456 3.285 1.303.629 3.138 1.018 4.873.909 1.013-.064 1.993-.297 2.813-.752 1.231-.681 1.963-1.775 2.116-3.163.06-.542.1-1.042.136-1.536-1.103.923-2.47 1.487-3.95 1.58-.146.011-.291.015-.435.015z" />
|
||||
<path fill="#65471B" d="M12.159 15.283c.113 0 .227-.004.341-.011 1.715-.108 3.191-1.013 4.088-2.332l.039-.068c-.031-2.988-.68-4.739-1.977-5.289-.934-.397-3.687-.31-7.401 2.859-.122.104-.227.211-.343.316.404 2.588 2.619 4.525 5.253 4.525z" />
|
||||
<path fill="#F4900C" d="M16.626 12.872l-.039.068c-.897 1.319-2.373 2.224-4.088 2.332-.114.007-.228.011-.341.011-2.634 0-4.849-1.937-5.253-4.524-.47.429-.887.866-1.241 1.309.903 2.745 3.485 4.715 6.494 4.715.144 0 .289-.005.435-.014 1.48-.093 2.847-.657 3.95-1.58.062-.841.091-1.614.083-2.317z" />
|
||||
<path fill="#FFF" d="M9.781 11.81c.61-.038 1.074-.564 1.035-1.174-.038-.61-.564-1.074-1.174-1.036-.61.038-1.074.564-1.036 1.174.039.61.565 1.074 1.175 1.036z" />
|
||||
</svg>
|
||||
),
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: async () => {
|
||||
try {
|
||||
Handler.translateMessage(msg, settings.store["decode-layers"]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Toasts.show(
|
||||
{
|
||||
id: Toasts.genId(),
|
||||
message: e.message,
|
||||
type: Toasts.Type.MESSAGE
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
addAccessory("bottom", props => {
|
||||
try {
|
||||
if (!Handler.cache[props.message.channel_id][props.message.id].top) {
|
||||
try {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Indicator layers={Handler.cache[props.message.channel_id][props.message.id].layers ?? 0} bottom={!Handler.isTranslated(props.message)} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
});
|
||||
|
||||
this.preSend = addPreSendListener((_, msg) => {
|
||||
if (settings.store["auto-encode-send"]) {
|
||||
const sendType = settings.store["encode-send-type"];
|
||||
var { content } = msg;
|
||||
|
||||
switch (sendType) {
|
||||
case 0: // all
|
||||
content = Bottom.encode(content);
|
||||
break;
|
||||
case 1: // inline greedy
|
||||
var prefix = escapeRegex(settings.store["inline-bottom-prefix"]);
|
||||
var suffix = escapeRegex(settings.store["inline-bottom-suffix"]);
|
||||
var reg = new RegExp(`${prefix}(.+)${suffix}`, "gm");
|
||||
content = content.replace(reg, (str, p1, o, s) => Bottom.encode(p1));
|
||||
break;
|
||||
case 2: // inline parsed
|
||||
var prefix = settings.store["inline-bottom-prefix"];
|
||||
var suffix = settings.store["inline-bottom-prefix"];
|
||||
content = inlineEncode(prefix, suffix, content);
|
||||
break;
|
||||
}
|
||||
msg.content = content;
|
||||
}
|
||||
});
|
||||
},
|
||||
stop() {
|
||||
removeButton("bottom");
|
||||
removeAccessory("bottom");
|
||||
removePreSendListener(this.preSend);
|
||||
},
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: "bottom",
|
||||
description: "Translate and send text as bottom 🥺",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => ({
|
||||
content: Bottom.encode(findOption(opts, "message", "")),
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
7
src/plugins/clientTheme/README.md
Normal file
7
src/plugins/clientTheme/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Classic Client Theme
|
||||
|
||||
Revival of the old client theme experiment (The one that came before the sucky one that we actually got)
|
||||
|
||||
![the ClientTheme theme colour picker](https://user-images.githubusercontent.com/37855219/230238053-e90b7098-373a-459a-bb8c-c24e82f69270.png)
|
||||
|
||||
https://github.com/Vendicated/Vencord/assets/45497981/6c1bcb3b-e0c7-4a02-b0b8-c4c5cd954f38
|
24
src/plugins/clientTheme/clientTheme.css
Normal file
24
src/plugins/clientTheme/clientTheme.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.client-theme-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.client-theme-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.client-theme-settings-labels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.client-theme-container > [class^="colorSwatch"] > [class^="swatch"] {
|
||||
border: thin solid var(--background-modifier-accent) !important;
|
||||
}
|
||||
|
||||
.client-theme-warning {
|
||||
color: var(--text-danger);
|
||||
}
|
214
src/plugins/clientTheme/index.tsx
Normal file
214
src/plugins/clientTheme/index.tsx
Normal file
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./clientTheme.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getTheme, Theme } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Forms } from "@webpack/common";
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR");
|
||||
|
||||
const colorPresets = [
|
||||
"#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",
|
||||
"#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42",
|
||||
"#3C2E42", "#422938"
|
||||
];
|
||||
|
||||
function onPickColor(color: number) {
|
||||
const hexColor = color.toString(16).padStart(6, "0");
|
||||
|
||||
settings.store.color = hexColor;
|
||||
updateColorVars(hexColor);
|
||||
}
|
||||
|
||||
function ThemeSettings() {
|
||||
const lightnessWarning = hexToLightness(settings.store.color) > 45;
|
||||
const lightModeWarning = getTheme() === Theme.Light;
|
||||
|
||||
return (
|
||||
<div className="client-theme-settings">
|
||||
<div className="client-theme-container">
|
||||
<div className="client-theme-settings-labels">
|
||||
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
|
||||
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
|
||||
</div>
|
||||
<ColorPicker
|
||||
color={parseInt(settings.store.color, 16)}
|
||||
onChange={onPickColor}
|
||||
showEyeDropper={false}
|
||||
suggestedColors={colorPresets}
|
||||
/>
|
||||
</div>
|
||||
{lightnessWarning || lightModeWarning
|
||||
? <div>
|
||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||
<Forms.FormText className="client-theme-warning">Your theme won't look good:</Forms.FormText>
|
||||
{lightnessWarning && <Forms.FormText className="client-theme-warning">Selected color is very light</Forms.FormText>}
|
||||
{lightModeWarning && <Forms.FormText className="client-theme-warning">Light mode isn't supported</Forms.FormText>}
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
color: {
|
||||
description: "Color your Discord client theme will be based around. Light mode isn't supported",
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: () => <ThemeSettings />
|
||||
},
|
||||
resetColor: {
|
||||
description: "Reset Theme Color",
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: () => (
|
||||
<Button onClick={() => onPickColor(0x313338)}>
|
||||
Reset Theme Color
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "ClientTheme",
|
||||
authors: [Devs.F53, Devs.Nuckyz],
|
||||
description: "Recreation of the old client theme experiment. Add a color to your Discord client theme",
|
||||
settings,
|
||||
|
||||
startAt: StartAt.DOMContentLoaded,
|
||||
start() {
|
||||
updateColorVars(settings.store.color);
|
||||
generateColorOffsets();
|
||||
},
|
||||
|
||||
stop() {
|
||||
document.getElementById("clientThemeVars")?.remove();
|
||||
document.getElementById("clientThemeOffsets")?.remove();
|
||||
}
|
||||
});
|
||||
|
||||
const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g;
|
||||
|
||||
async function generateColorOffsets() {
|
||||
|
||||
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
const variableLightness = {} as Record<string, number>;
|
||||
|
||||
// Search all stylesheets for color variables
|
||||
for (const styleLinkNode of styleLinkNodes) {
|
||||
const cssLink = styleLinkNode.getAttribute("href");
|
||||
if (!cssLink) continue;
|
||||
|
||||
const res = await fetch(cssLink);
|
||||
const cssString = await res.text();
|
||||
|
||||
// Get lightness values of --primary variables >=500
|
||||
let variableMatch = variableRegex.exec(cssString);
|
||||
while (variableMatch !== null) {
|
||||
const [, variable, lightness] = variableMatch;
|
||||
variableLightness[variable] = parseFloat(lightness);
|
||||
variableMatch = variableRegex.exec(cssString);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate offsets
|
||||
const lightnessOffsets = Object.entries(variableLightness)
|
||||
.map(([key, lightness]) => {
|
||||
const lightnessOffset = lightness - variableLightness["--primary-600-hsl"];
|
||||
const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
|
||||
return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("id", "clientThemeOffsets");
|
||||
style.textContent = `:root:root {
|
||||
${lightnessOffsets}
|
||||
}`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function updateColorVars(color: string) {
|
||||
const { hue, saturation, lightness } = hexToHSL(color);
|
||||
|
||||
let style = document.getElementById("clientThemeVars");
|
||||
if (!style) {
|
||||
style = document.createElement("style");
|
||||
style.setAttribute("id", "clientThemeVars");
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
style.textContent = `:root {
|
||||
--theme-h: ${hue};
|
||||
--theme-s: ${saturation}%;
|
||||
--theme-l: ${lightness}%;
|
||||
}`;
|
||||
}
|
||||
|
||||
// https://css-tricks.com/converting-color-spaces-in-javascript/
|
||||
function hexToHSL(hexCode: string) {
|
||||
// Hex => RGB normalized to 0-1
|
||||
const r = parseInt(hexCode.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hexCode.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hexCode.substring(4, 6), 16) / 255;
|
||||
|
||||
// RGB => HSL
|
||||
const cMax = Math.max(r, g, b);
|
||||
const cMin = Math.min(r, g, b);
|
||||
const delta = cMax - cMin;
|
||||
|
||||
let hue: number, saturation: number, lightness: number;
|
||||
|
||||
lightness = (cMax + cMin) / 2;
|
||||
|
||||
if (delta === 0) {
|
||||
// If r=g=b then the only thing that matters is lightness
|
||||
hue = 0;
|
||||
saturation = 0;
|
||||
} else {
|
||||
// Magic
|
||||
saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
||||
|
||||
if (cMax === r)
|
||||
hue = ((g - b) / delta) % 6;
|
||||
else if (cMax === g)
|
||||
hue = (b - r) / delta + 2;
|
||||
else
|
||||
hue = (r - g) / delta + 4;
|
||||
hue *= 60;
|
||||
if (hue < 0)
|
||||
hue += 360;
|
||||
}
|
||||
|
||||
// Move saturation and lightness from 0-1 to 0-100
|
||||
saturation *= 100;
|
||||
lightness *= 100;
|
||||
|
||||
return { hue, saturation, lightness };
|
||||
}
|
||||
|
||||
// Minimized math just for lightness, lowers lag when changing colors
|
||||
function hexToLightness(hexCode: string) {
|
||||
// Hex => RGB normalized to 0-1
|
||||
const r = parseInt(hexCode.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hexCode.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hexCode.substring(4, 6), 16) / 255;
|
||||
|
||||
const cMax = Math.max(r, g, b);
|
||||
const cMin = Math.min(r, g, b);
|
||||
|
||||
const lightness = 100 * ((cMax + cMin) / 2);
|
||||
|
||||
return lightness;
|
||||
}
|
|
@ -62,23 +62,27 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
let fakeRenderWin: WeakRef<Window> | undefined;
|
||||
const find = newFindWrapper(f => f);
|
||||
return {
|
||||
...Vencord.Webpack.Common,
|
||||
wp: Vencord.Webpack,
|
||||
wpc: Webpack.wreq.c,
|
||||
wreq: Webpack.wreq,
|
||||
wpsearch: search,
|
||||
wpex: extract,
|
||||
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
||||
find: newFindWrapper(f => f),
|
||||
wpexs: (code: string) => extract(Webpack.findModuleId(code)!),
|
||||
find,
|
||||
findAll,
|
||||
findByProps: newFindWrapper(filters.byProps),
|
||||
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
|
||||
findByCode: newFindWrapper(filters.byCode),
|
||||
findAllByCode: (code: string) => findAll(filters.byCode(code)),
|
||||
findComponentByCode: newFindWrapper(filters.componentByCode),
|
||||
findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),
|
||||
findExportedComponent: (...props: string[]) => find(...props)[props[0]],
|
||||
findStore: newFindWrapper(filters.byStoreName),
|
||||
PluginsApi: Vencord.Plugins,
|
||||
plugins: Vencord.Plugins.plugins,
|
||||
React,
|
||||
Settings: Vencord.Settings,
|
||||
Api: Vencord.Api,
|
||||
reload: () => location.reload(),
|
||||
|
@ -92,7 +96,25 @@ export default definePlugin({
|
|||
fakeRenderWin = new WeakRef(win);
|
||||
win.focus();
|
||||
|
||||
ReactDOM.render(React.createElement(component, props), win.document.body);
|
||||
const doc = win.document;
|
||||
doc.body.style.margin = "1em";
|
||||
|
||||
if (!win.prepared) {
|
||||
win.prepared = true;
|
||||
|
||||
[...document.querySelectorAll("style"), ...document.querySelectorAll("link[rel=stylesheet]")].forEach(s => {
|
||||
const n = s.cloneNode(true) as HTMLStyleElement | HTMLLinkElement;
|
||||
|
||||
if (s.parentElement?.tagName === "HEAD")
|
||||
doc.head.append(n);
|
||||
else if (n.id?.startsWith("vencord-") || n.id?.startsWith("vcd-"))
|
||||
doc.documentElement.append(n);
|
||||
else
|
||||
doc.body.append(n);
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.render(React.createElement(component, props), doc.body.appendChild(document.createElement("div")));
|
||||
}
|
||||
};
|
||||
},
|
||||
|
|
|
@ -22,10 +22,10 @@ import { Devs } from "@utils/constants";
|
|||
import { isTruthy } from "@utils/guards";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
|
||||
|
||||
const ActivityComponent = findByCodeLazy("onOpenGameProfile");
|
||||
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
||||
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
|
||||
const Colors = findByPropsLazy("profileColors");
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ async function embedDidMount(this: Component<Props>) {
|
|||
const { titles, thumbnails } = await res.json();
|
||||
|
||||
const hasTitle = titles[0]?.votes >= 0;
|
||||
const hasThumb = thumbnails[0]?.votes >= 0;
|
||||
const hasThumb = thumbnails[0]?.votes >= 0 && !thumbnails[0].original;
|
||||
|
||||
if (!hasTitle && !hasThumb) return;
|
||||
|
||||
|
@ -58,12 +58,12 @@ async function embedDidMount(this: Component<Props>) {
|
|||
enabled: true
|
||||
};
|
||||
|
||||
if (titles[0]?.votes >= 0) {
|
||||
if (hasTitle) {
|
||||
embed.dearrow.oldTitle = embed.rawTitle;
|
||||
embed.rawTitle = titles[0].title;
|
||||
}
|
||||
|
||||
if (thumbnails[0]?.votes >= 0) {
|
||||
if (hasThumb) {
|
||||
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
||||
}
|
||||
|
|
|
@ -77,15 +77,6 @@ export default definePlugin({
|
|||
}
|
||||
]
|
||||
},
|
||||
// Fix search history being disabled / broken with isStaff
|
||||
{
|
||||
find: '("showNewSearch")',
|
||||
predicate: () => settings.store.enableIsStaff,
|
||||
replacement: {
|
||||
match: /(?<=showNewSearch"\);return)\s?/,
|
||||
replace: "!1&&"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'H1,title:"Experiments"',
|
||||
replacement: {
|
||||
|
|
|
@ -206,10 +206,10 @@ export default definePlugin({
|
|||
},
|
||||
// Allow stickers to be sent everywhere
|
||||
{
|
||||
find: "canUseStickersEverywhere:function",
|
||||
find: "canUseCustomStickersEverywhere:function",
|
||||
predicate: () => settings.store.enableStickerBypass,
|
||||
replacement: {
|
||||
match: /canUseStickersEverywhere:function\(\i\){/,
|
||||
match: /canUseCustomStickersEverywhere:function\(\i\){/,
|
||||
replace: "$&return true;"
|
||||
},
|
||||
},
|
||||
|
|
27
src/plugins/fixSpotifyEmbeds.desktop/native.ts
Normal file
27
src/plugins/fixSpotifyEmbeds.desktop/native.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { getSettings } from "main/ipcMain";
|
||||
|
||||
app.on("browser-window-created", (_, win) => {
|
||||
win.webContents.on("frame-created", (_, { frame }) => {
|
||||
frame.once("dom-ready", () => {
|
||||
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
||||
const settings = getSettings().plugins?.FixSpotifyEmbeds;
|
||||
if (!settings?.enabled) return;
|
||||
|
||||
frame.executeJavaScript(`
|
||||
const original = Audio.prototype.play;
|
||||
Audio.prototype.play = function() {
|
||||
this.volume = ${(settings.volume / 100) || 0.1};
|
||||
return original.apply(this, arguments);
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -23,7 +23,7 @@ import { findByPropsLazy } from "@webpack";
|
|||
import { RestAPI, UserStore } from "@webpack/common";
|
||||
|
||||
const FriendInvites = findByPropsLazy("createFriendInvite");
|
||||
const uuid = findByPropsLazy("v4", "v1");
|
||||
const { uuid4 } = findByPropsLazy("uuid4");
|
||||
|
||||
export default definePlugin({
|
||||
name: "FriendInvites",
|
||||
|
@ -56,7 +56,7 @@ export default definePlugin({
|
|||
|
||||
let invite: any;
|
||||
if (uses === 1) {
|
||||
const random = uuid.v4();
|
||||
const random = uuid4();
|
||||
const { body: { invite_suggestions } } = await RestAPI.post({
|
||||
url: "/friend-finder/find-friends",
|
||||
body: {
|
||||
|
|
|
@ -20,12 +20,12 @@ import { disableStyle, enableStyle } from "@api/Styles";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { StatusSettingsStores } from "@webpack/common";
|
||||
|
||||
import style from "./style.css?managed";
|
||||
|
||||
const Button = findByCodeLazy("Button.Sizes.NONE,disabled:");
|
||||
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
|
||||
|
||||
function makeIcon(showCurrentGame?: boolean) {
|
||||
return function () {
|
||||
|
|
|
@ -19,11 +19,9 @@
|
|||
import { Devs } from "@utils/constants";
|
||||
import { insertTextIntoChatInputBox } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
import { filters, mapMangledModuleLazy } from "@webpack";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
|
||||
const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', {
|
||||
close: filters.byCode("activeView:null", "setState")
|
||||
});
|
||||
const { closeExpressionPicker } = findByPropsLazy("closeExpressionPicker");
|
||||
|
||||
export default definePlugin({
|
||||
name: "GifPaste",
|
||||
|
@ -41,7 +39,7 @@ export default definePlugin({
|
|||
handleSelect(gif?: { url: string; }) {
|
||||
if (gif) {
|
||||
insertTextIntoChatInputBox(gif.url + " ");
|
||||
ExpressionPickerState.close();
|
||||
closeExpressionPicker();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,10 +18,9 @@
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByProps, findByPropsLazy } from "@webpack";
|
||||
import { ContextMenu, FluxDispatcher, Menu } from "@webpack/common";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { ContextMenuApi, FluxDispatcher, Menu } from "@webpack/common";
|
||||
import { Channel, Message } from "discord-types/general";
|
||||
|
||||
interface Sticker {
|
||||
|
@ -51,7 +50,7 @@ const settings = definePluginSettings({
|
|||
}>();
|
||||
|
||||
const MessageActions = findByPropsLazy("sendGreetMessage");
|
||||
const WELCOME_STICKERS = proxyLazy(() => findByProps("WELCOME_STICKERS")?.WELCOME_STICKERS);
|
||||
const { WELCOME_STICKERS } = findByPropsLazy("WELCOME_STICKERS");
|
||||
|
||||
function greet(channel: Channel, message: Message, stickers: string[]) {
|
||||
const options = MessageActions.getSendMessageOptionsForReply({
|
||||
|
@ -184,6 +183,6 @@ export default definePlugin({
|
|||
}
|
||||
) {
|
||||
if (!(props.message as any).deleted)
|
||||
ContextMenu.open(event, () => <GreetMenu {...props} />);
|
||||
ContextMenuApi.openContextMenu(event, () => <GreetMenu {...props} />);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@ import * as DataStore from "@api/DataStore";
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { useForceUpdater } from "@utils/react";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { StatusSettingsStores, Tooltip } from "webpack/common";
|
||||
|
@ -27,14 +26,12 @@ interface IgnoredActivity {
|
|||
const RunningGameStore = findStoreLazy("RunningGameStore");
|
||||
|
||||
function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {
|
||||
const forceUpdate = useForceUpdater();
|
||||
|
||||
return (
|
||||
<Tooltip text={tooltipText}>
|
||||
{tooltipProps => (
|
||||
<button
|
||||
{...tooltipProps}
|
||||
onClick={e => handleActivityToggle(e, activity, forceUpdate)}
|
||||
onClick={e => handleActivityToggle(e, activity)}
|
||||
style={{ all: "unset", cursor: "pointer", display: "flex", justifyContent: "center", alignItems: "center" }}
|
||||
>
|
||||
<svg
|
||||
|
@ -54,11 +51,14 @@ const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(act
|
|||
const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable Activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill);
|
||||
|
||||
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
|
||||
if (getIgnoredActivities().some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
|
||||
const s = settings.use(["ignoredActivities"]);
|
||||
const { ignoredActivities = [] } = s;
|
||||
|
||||
if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
|
||||
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
|
||||
}
|
||||
|
||||
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity, forceUpdateButton: () => void) {
|
||||
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {
|
||||
e.stopPropagation();
|
||||
|
||||
const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id);
|
||||
|
@ -67,7 +67,6 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|||
|
||||
// Trigger activities recalculation
|
||||
StatusSettingsStores.ShowCurrentGame.updateSetting(old => old);
|
||||
forceUpdateButton();
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({}).withPrivateSettings<{
|
||||
|
@ -90,8 +89,8 @@ export default definePlugin({
|
|||
find: '.displayName="LocalActivityStore"',
|
||||
replacement: [
|
||||
{
|
||||
match: /LISTENING.+?}\),(?<=(\i)\.push.+?)/,
|
||||
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored),`
|
||||
match: /HANG_STATUS.+?(?=!\i\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
|
||||
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ import { makeRange } from "@components/PluginSettings/components";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ContextMenu, Menu, React, ReactDOM } from "@webpack/common";
|
||||
import { ContextMenuApi, Menu, React, ReactDOM } from "@webpack/common";
|
||||
import type { Root } from "react-dom/client";
|
||||
|
||||
import { Magnifier, MagnifierProps } from "./components/Magnifier";
|
||||
|
@ -89,7 +89,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
|||
checked={settings.store.square}
|
||||
action={() => {
|
||||
settings.store.square = !settings.store.square;
|
||||
ContextMenu.close();
|
||||
ContextMenuApi.closeContextMenu();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuCheckboxItem
|
||||
|
@ -98,7 +98,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
|||
checked={settings.store.nearestNeighbour}
|
||||
action={() => {
|
||||
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
|
||||
ContextMenu.close();
|
||||
ContextMenuApi.closeContextMenu();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuControlItem
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import { registerCommand, unregisterCommand } from "@api/Commands";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Patch, Plugin } from "@utils/types";
|
||||
import { Patch, Plugin, StartAt } from "@utils/types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
import { FluxEvents } from "@webpack/types";
|
||||
|
||||
|
@ -85,9 +85,15 @@ for (const p of pluginsValues) {
|
|||
}
|
||||
}
|
||||
|
||||
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins() {
|
||||
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins(target: StartAt) {
|
||||
logger.info(`Starting plugins (stage ${target})`);
|
||||
for (const name in Plugins)
|
||||
if (isPluginEnabled(name)) {
|
||||
const p = Plugins[name];
|
||||
|
||||
const startAt = p.startAt ?? StartAt.WebpackReady;
|
||||
if (startAt !== target) continue;
|
||||
|
||||
startPlugin(Plugins[name]);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -22,9 +22,8 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants.js";
|
||||
import { classes } from "@utils/misc";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { find, findByCode, findByPropsLazy } from "@webpack";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import {
|
||||
Button,
|
||||
ChannelStore,
|
||||
|
@ -45,9 +44,9 @@ const messageCache = new Map<string, {
|
|||
fetched: boolean;
|
||||
}>();
|
||||
|
||||
const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed"));
|
||||
const AutoModEmbed = LazyComponent(() => findByCode(".withFooter]:", "childrenMessageContent:"));
|
||||
const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes("renderSimpleAccessories)")));
|
||||
const Embed = findComponentByCodeLazy(".inlineMediaEmbed");
|
||||
const AutoModEmbed = findComponentByCodeLazy(".withFooter]:", "childrenMessageContent:");
|
||||
const ChannelMessage = findComponentByCodeLazy("renderSimpleAccessories)");
|
||||
|
||||
const SearchResultClasses = findByPropsLazy("message", "searchResult");
|
||||
|
||||
|
|
|
@ -302,6 +302,7 @@ export default definePlugin({
|
|||
match: /attachments:(\i)\((\i)\)/,
|
||||
replace:
|
||||
"attachments: $1((() => {" +
|
||||
" if ($self.shouldIgnore($2)) return $2;" +
|
||||
" let old = arguments[1]?.attachments;" +
|
||||
" if (!old) return $2;" +
|
||||
" let new_ = $2.attachments?.map(a => a.id) ?? [];" +
|
||||
|
|
|
@ -25,10 +25,6 @@ import definePlugin, { OptionType } from "@utils/types";
|
|||
const EMOTE = "<:luna:1035316192220553236>";
|
||||
const DATA_KEY = "MessageTags_TAGS";
|
||||
const MessageTagsMarker = Symbol("MessageTags");
|
||||
const author = {
|
||||
id: "821472922140803112",
|
||||
bot: false
|
||||
};
|
||||
|
||||
interface Tag {
|
||||
name: string;
|
||||
|
@ -59,14 +55,12 @@ function createTagCommand(tag: Tag) {
|
|||
execute: async (_, ctx) => {
|
||||
if (!await getTag(tag.name)) {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)`
|
||||
});
|
||||
return { content: `/${tag.name}` };
|
||||
}
|
||||
|
||||
if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: `${EMOTE} The tag **${tag.name}** has been sent!`
|
||||
});
|
||||
return { content: tag.message.replaceAll("\\n", "\n") };
|
||||
|
@ -162,7 +156,6 @@ export default definePlugin({
|
|||
|
||||
if (await getTag(name))
|
||||
return sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: `${EMOTE} A Tag with the name **${name}** already exists!`
|
||||
});
|
||||
|
||||
|
@ -176,7 +169,6 @@ export default definePlugin({
|
|||
await addTag(tag);
|
||||
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: `${EMOTE} Successfully created the tag **${name}**!`
|
||||
});
|
||||
break; // end 'create'
|
||||
|
@ -186,7 +178,6 @@ export default definePlugin({
|
|||
|
||||
if (!await getTag(name))
|
||||
return sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: `${EMOTE} A Tag with the name **${name}** does not exist!`
|
||||
});
|
||||
|
||||
|
@ -194,14 +185,12 @@ export default definePlugin({
|
|||
await removeTag(name);
|
||||
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: `${EMOTE} Successfully deleted the tag **${name}**!`
|
||||
});
|
||||
break; // end 'delete'
|
||||
}
|
||||
case "list": {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
embeds: [
|
||||
{
|
||||
// @ts-ignore
|
||||
|
@ -224,12 +213,10 @@ export default definePlugin({
|
|||
|
||||
if (!tag)
|
||||
return sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: `${EMOTE} A Tag with the name **${name}** does not exist!`
|
||||
});
|
||||
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: tag.message.replaceAll("\\n", "\n")
|
||||
});
|
||||
break; // end 'preview'
|
||||
|
@ -237,7 +224,6 @@ export default definePlugin({
|
|||
|
||||
default: {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
author,
|
||||
content: "Invalid sub-command"
|
||||
});
|
||||
break;
|
||||
|
|
|
@ -26,15 +26,13 @@ export default definePlugin({
|
|||
authors: [Devs.Ven, Devs.adryd],
|
||||
|
||||
start() {
|
||||
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/5977144dce83e4d71af1de005d16e38eebeb7b72/oneko.js")
|
||||
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/8fa8a1864aa71cd7a794d58bc139e755e96a236c/oneko.js")
|
||||
.then(x => x.text())
|
||||
.then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif"))
|
||||
.then(eval);
|
||||
},
|
||||
|
||||
stop() {
|
||||
clearInterval(window.onekoInterval);
|
||||
delete window.onekoInterval;
|
||||
document.getElementById("oneko")?.remove();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import definePlugin, { OptionType, PluginNative } from "@utils/types";
|
||||
import { showToast, Toasts } from "@webpack/common";
|
||||
import type { MouseEvent } from "react";
|
||||
|
||||
|
@ -45,6 +45,8 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof import("./native")>;
|
||||
|
||||
export default definePlugin({
|
||||
name: "OpenInApp",
|
||||
description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser",
|
||||
|
@ -84,7 +86,7 @@ export default definePlugin({
|
|||
if (!IS_WEB && ShortUrlMatcher.test(url)) {
|
||||
event?.preventDefault();
|
||||
// CORS jumpscare
|
||||
url = await VencordNative.pluginHelpers.OpenInApp.resolveRedirect(url);
|
||||
url = await Native.resolveRedirect(url);
|
||||
}
|
||||
|
||||
spotify: {
|
||||
|
|
31
src/plugins/openInApp/native.ts
Normal file
31
src/plugins/openInApp/native.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { IpcMainInvokeEvent } from "electron";
|
||||
import { request } from "https";
|
||||
|
||||
// These links don't support CORS, so this has to be native
|
||||
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||
|
||||
function getRedirect(url: string) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const req = request(new URL(url), { method: "HEAD" }, res => {
|
||||
resolve(
|
||||
res.headers.location
|
||||
? getRedirect(res.headers.location)
|
||||
: url
|
||||
);
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveRedirect(_: IpcMainInvokeEvent, url: string) {
|
||||
if (!validRedirectUrls.test(url)) return url;
|
||||
|
||||
return getRedirect(url);
|
||||
}
|
|
@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
|
|||
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||
import { getUniqueUsername } from "@utils/discord";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { ContextMenu, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import type { Guild } from "discord-types/general";
|
||||
|
||||
import { settings } from "..";
|
||||
|
@ -111,7 +111,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
|||
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
|
||||
onContextMenu={e => {
|
||||
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
|
||||
ContextMenu.open(e, () => (
|
||||
ContextMenuApi.openContextMenu(e, () => (
|
||||
<RoleContextMenu
|
||||
guild={guild}
|
||||
roleId={permission.id!}
|
||||
|
@ -194,7 +194,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
|||
return (
|
||||
<Menu.Menu
|
||||
navId={cl("role-context-menu")}
|
||||
onClose={ContextMenu.close}
|
||||
onClose={ContextMenuApi.closeContextMenu}
|
||||
aria-label="Role Options"
|
||||
>
|
||||
<Menu.MenuItem
|
||||
|
|
|
@ -46,7 +46,7 @@ const Classes = proxyLazy(() => {
|
|||
return Object.assign({}, ...modules);
|
||||
}) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>;
|
||||
|
||||
function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) {
|
||||
function UserPermissionsComponent({ guild, guildMember, showBorder }: { guild: Guild; guildMember: GuildMember; showBorder: boolean; }) {
|
||||
const stns = settings.use(["permissionsSortOrder"]);
|
||||
|
||||
const [rolePermissions, userPermissions] = useMemo(() => {
|
||||
|
@ -76,7 +76,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
|
|||
sortUserRoles(userRoles);
|
||||
|
||||
for (const [permission, bit] of Object.entries(PermissionsBits)) {
|
||||
for (const { permissions, colorString, position, name } of userRoles) {
|
||||
for (const { permissions, colorString, position } of userRoles) {
|
||||
if ((permissions & bit) === bit) {
|
||||
userPermissions.push({
|
||||
permission: getPermissionString(permission),
|
||||
|
@ -133,7 +133,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
|
|||
{userPermissions.length > 0 && (
|
||||
<div className={classes(root, roles)}>
|
||||
{userPermissions.map(({ permission, roleColor }) => (
|
||||
<div className={classes(role, rolePill, rolePillBorder)}>
|
||||
<div className={classes(role, rolePill, showBorder ? rolePillBorder : null)}>
|
||||
<div className={roleRemoveButton}>
|
||||
<span
|
||||
className={roleCircle}
|
||||
|
|
|
@ -163,13 +163,13 @@ export default definePlugin({
|
|||
{
|
||||
find: ".popularApplicationCommandIds,",
|
||||
replacement: {
|
||||
match: /showBorder:.{0,60}}\),(?<=guild:(\i),guildMember:(\i),.+?)/,
|
||||
replace: (m, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember}),`
|
||||
match: /showBorder:(.{0,60})}\),(?<=guild:(\i),guildMember:(\i),.+?)/,
|
||||
replace: (m, showBoder, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember},${showBoder}),`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
UserPermissions: (guild: Guild, guildMember?: GuildMember) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} />,
|
||||
UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBoder} />,
|
||||
|
||||
userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User),
|
||||
channelContextMenuPatch: makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel),
|
||||
|
|
|
@ -100,10 +100,10 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Fix getRowHeight's check for whether this is the DMs section
|
||||
// section === DMS
|
||||
match: /===\i\.DMS&&0/,
|
||||
// section -1 === DMS
|
||||
replace: "-1$&"
|
||||
// DMS (inlined) === section
|
||||
match: /(?<=getRowHeight=\(.{2,50}?)1===\i/,
|
||||
// DMS (inlined) === section - 1
|
||||
replace: "$&-1"
|
||||
},
|
||||
{
|
||||
// Override scrollToChannel to properly account for pinned channels
|
||||
|
|
|
@ -20,12 +20,12 @@ import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCal
|
|||
import { ReplyIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { ChannelStore, i18n, Menu, PermissionsBits, PermissionStore, SelectedChannelStore } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
|
||||
const replyFn = findByCodeLazy("showMentionToggle", "TEXTAREA_FOCUS", "shiftKey");
|
||||
const messageUtils = findByPropsLazy("replyToMessage");
|
||||
|
||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => {
|
||||
// make sure the message is in the selected channel
|
||||
|
@ -43,7 +43,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
|
|||
id="reply"
|
||||
label={i18n.Messages.MESSAGE_ACTION_REPLY}
|
||||
icon={ReplyIcon}
|
||||
action={(e: React.MouseEvent) => replyFn(channel, message, e)}
|
||||
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
|
|||
id="reply"
|
||||
label={i18n.Messages.MESSAGE_ACTION_REPLY}
|
||||
icon={ReplyIcon}
|
||||
action={(e: React.MouseEvent) => replyFn(channel, message, e)}
|
||||
action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
|
|
@ -10,14 +10,14 @@ import { classNameFactory } from "@api/Styles";
|
|||
import { openImageModal, openUserProfile } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { LazyComponent, useAwaiter } from "@utils/react";
|
||||
import { findByProps, findByPropsLazy } from "@webpack";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { findByPropsLazy, findExportedComponentLazy } from "@webpack";
|
||||
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
||||
import { Guild, User } from "discord-types/general";
|
||||
|
||||
const IconUtils = findByPropsLazy("getGuildBannerURL");
|
||||
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
||||
const FriendRow = LazyComponent(() => findByProps("FriendRow").FriendRow);
|
||||
const FriendRow = findExportedComponentLazy("FriendRow");
|
||||
|
||||
const cl = classNameFactory("vc-gp-");
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ export default definePlugin({
|
|||
createHighlighter,
|
||||
renderHighlighter: ({ lang, content }: { lang: string; content: string; }) => {
|
||||
return createHighlighter({
|
||||
lang,
|
||||
lang: lang?.toLowerCase(),
|
||||
content,
|
||||
isPreview: false,
|
||||
});
|
||||
|
|
|
@ -16,12 +16,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { findByCode, findLazy } from "@webpack";
|
||||
import { findComponentByCodeLazy, findLazy } from "@webpack";
|
||||
import { i18n, useToken } from "@webpack/common";
|
||||
|
||||
const ColorMap = findLazy(m => m.colors?.INTERACTIVE_MUTED?.css);
|
||||
const VerifiedIconComponent = LazyComponent(() => findByCode(".CONNECTIONS_ROLE_OFFICIAL_ICON_TOOLTIP"));
|
||||
const VerifiedIconComponent = findComponentByCodeLazy(".CONNECTIONS_ROLE_OFFICIAL_ICON_TOOLTIP");
|
||||
|
||||
export function VerifiedIcon() {
|
||||
const color = useToken(ColorMap.colors.INTERACTIVE_MUTED).hex();
|
||||
|
|
|
@ -24,15 +24,14 @@ import { Flex } from "@components/Flex";
|
|||
import { CopyIcon, LinkIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCode, findByCodeLazy, findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Text, Tooltip, UserProfileStore } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import { VerifiedIcon } from "./VerifiedIcon";
|
||||
|
||||
const Section = LazyComponent(() => findByCode(".lastSection]:"));
|
||||
const Section = findComponentByCodeLazy(".lastSection", "children:");
|
||||
const ThemeStore = findStoreLazy("ThemeStore");
|
||||
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
|
||||
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');
|
||||
|
|
|
@ -18,9 +18,8 @@
|
|||
|
||||
import { Settings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { formatDuration } from "@utils/text";
|
||||
import { find, findByCode, findByPropsLazy } from "@webpack";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findComponentLazy } from "@webpack";
|
||||
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
|
||||
import type { Channel } from "discord-types/general";
|
||||
|
||||
|
@ -81,17 +80,17 @@ const enum ChannelFlags {
|
|||
|
||||
const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase");
|
||||
const ChatClasses = findByPropsLazy("chat", "content", "noChat", "chatContent");
|
||||
const ChannelBeginHeader = LazyComponent(() => findByCode(".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE"));
|
||||
const TagComponent = LazyComponent(() => find(m => {
|
||||
const ChannelBeginHeader = findComponentByCodeLazy(".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE");
|
||||
const TagComponent = findComponentLazy(m => {
|
||||
if (typeof m !== "function") return false;
|
||||
|
||||
const code = Function.prototype.toString.call(m);
|
||||
// Get the component which doesn't include increasedActivity
|
||||
return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill");
|
||||
}));
|
||||
});
|
||||
|
||||
const EmojiParser = findByPropsLazy("convertSurrogateToName");
|
||||
const EmojiUtils = findByPropsLazy("getURL", "buildEmojiReactionColorsPlatformed");
|
||||
const EmojiUtils = findByPropsLazy("getURL", "getEmojiColors");
|
||||
|
||||
const ChannelTypesToChannelNames = {
|
||||
[ChannelTypes.GUILD_TEXT]: "text",
|
||||
|
|
|
@ -68,7 +68,7 @@ export default definePlugin({
|
|||
patches: [
|
||||
{
|
||||
// RenderLevel defines if a channel is hidden, collapsed in category, visible, etc
|
||||
find: ".CannotShow=",
|
||||
find: '"placeholder-channel-id"',
|
||||
replacement: [
|
||||
// Remove the special logic for channels we don't have access to
|
||||
{
|
||||
|
@ -77,18 +77,13 @@ export default definePlugin({
|
|||
},
|
||||
// Do not check for unreads when selecting the render level if the channel is hidden
|
||||
{
|
||||
match: /(?=!1===\i.\i\.hasRelevantUnread\(this\.record\))/,
|
||||
match: /(?=!\(0,\i\.getHasImportantUnread\)\(this\.record\))/,
|
||||
replace: "$self.isHiddenChannel(this.record)||"
|
||||
},
|
||||
// Make channels we dont have access to be the same level as normal ones
|
||||
{
|
||||
match: /(?<=renderLevel:(\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/,
|
||||
replace: (_, renderLevelExpression) => renderLevelExpression
|
||||
},
|
||||
// Make channels we dont have access to be the same level as normal ones
|
||||
{
|
||||
match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(\i)\..+?(?=,)/,
|
||||
replace: (_, RenderLevels) => `${RenderLevels}.Show`
|
||||
match: /(activeJoinedRelevantThreads:.{0,50}VIEW_CHANNEL.+?renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g,
|
||||
replace: (_, rest, defaultRenderLevel) => `${rest}${defaultRenderLevel}`
|
||||
},
|
||||
// Remove permission checking for getRenderLevel function
|
||||
{
|
||||
|
@ -157,7 +152,7 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: ".UNREAD_HIGHLIGHT",
|
||||
find: "UNREAD_IMPORTANT:",
|
||||
predicate: () => settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
|
||||
replacement: [
|
||||
// Make the channel appear as muted if it's hidden
|
||||
|
@ -178,7 +173,7 @@ export default definePlugin({
|
|||
]
|
||||
},
|
||||
{
|
||||
find: ".UNREAD_HIGHLIGHT",
|
||||
find: "UNREAD_IMPORTANT:",
|
||||
replacement: [
|
||||
{
|
||||
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
|
||||
|
@ -198,7 +193,7 @@ export default definePlugin({
|
|||
// Hide the new version of unreads box for hidden channels
|
||||
find: '.displayName="ChannelListUnreadsStore"',
|
||||
replacement: {
|
||||
match: /(?<=if\(null==(\i))(?=.{0,160}?hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module
|
||||
match: /(?<=if\(null==(\i))(?=.{0,160}?getHasImportantUnread\)\(\i\))/g, // Global because Discord has multiple methods like that in the same module
|
||||
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
|
||||
}
|
||||
},
|
||||
|
@ -206,7 +201,7 @@ export default definePlugin({
|
|||
// Make the old version of unreads box not visible for hidden channels
|
||||
find: "renderBottomUnread(){",
|
||||
replacement: {
|
||||
match: /(?=&&\i\.\i\.hasRelevantUnread\((\i\.record)\))/,
|
||||
match: /(?=&&\(0,\i\.getHasImportantUnread\)\((\i\.record)\))/,
|
||||
replace: "&&!$self.isHiddenChannel($1)"
|
||||
}
|
||||
},
|
||||
|
@ -214,7 +209,7 @@ export default definePlugin({
|
|||
// Make the state of the old version of unreads box not include hidden channels
|
||||
find: ".useFlattenedChannelIdListWithThreads)",
|
||||
replacement: {
|
||||
match: /(?=&&\i\.\i\.hasRelevantUnread\((\i)\))/,
|
||||
match: /(?=&&\(0,\i\.getHasImportantUnread\)\((\i)\))/,
|
||||
replace: "&&!$self.isHiddenChannel($1)"
|
||||
}
|
||||
},
|
||||
|
@ -260,7 +255,7 @@ export default definePlugin({
|
|||
{
|
||||
find: '"alt+shift+down"',
|
||||
replacement: {
|
||||
match: /(?<=getChannel\(\i\);return null!=(\i))(?=.{0,150}?hasRelevantUnread\(\i\))/,
|
||||
match: /(?<=getChannel\(\i\);return null!=(\i))(?=.{0,150}?getHasImportantUnread\)\(\i\))/,
|
||||
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
|
||||
}
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
|
|||
import { debounce } from "@utils/debounce";
|
||||
import { openImageModal } from "@utils/discord";
|
||||
import { classes, copyWithToast } from "@utils/misc";
|
||||
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { SpotifyStore, Track } from "./SpotifyStore";
|
||||
|
||||
|
@ -104,7 +104,7 @@ function CopyContextMenu({ name, path }: { name: string; path: string; }) {
|
|||
|
||||
function makeContextMenu(name: string, path: string) {
|
||||
return (e: React.MouseEvent<HTMLElement, MouseEvent>) =>
|
||||
ContextMenu.open(e, () => <CopyContextMenu name={name} path={path} />);
|
||||
ContextMenuApi.openContextMenu(e, () => <CopyContextMenu name={name} path={path} />);
|
||||
}
|
||||
|
||||
function Controls() {
|
||||
|
@ -277,7 +277,7 @@ function Info({ track }: { track: Track; }) {
|
|||
alt="Album Image"
|
||||
onClick={() => setCoverExpanded(!coverExpanded)}
|
||||
onContextMenu={e => {
|
||||
ContextMenu.open(e, () => <AlbumContextMenu track={track} />);
|
||||
ContextMenuApi.openContextMenu(e, () => <AlbumContextMenu track={track} />);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
11
src/plugins/superReactionTweaks/README.md
Normal file
11
src/plugins/superReactionTweaks/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Super Reaction Tweaks
|
||||
|
||||
This plugin applies configurable various tweaks to super reactions.
|
||||
|
||||
![Screenshot](https://user-images.githubusercontent.com/22851444/281598795-58f07116-9f95-4f64-940b-23a5499f2302.png)
|
||||
|
||||
## Features:
|
||||
|
||||
**Super React By Default** - The reaction picker will default to super reactions instead of normal reactions.
|
||||
|
||||
**Super Reaction Play Limit** - Allows you to decide how many super reaction animations can play at once, including removing the limit entirely.
|
63
src/plugins/superReactionTweaks/index.ts
Normal file
63
src/plugins/superReactionTweaks/index.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated, ant0n, FieryFlames 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";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
superReactByDefault: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Reaction picker will default to Super Reactions",
|
||||
default: true,
|
||||
},
|
||||
unlimitedSuperReactionPlaying: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Remove the limit on Super Reactions playing at once",
|
||||
default: false,
|
||||
},
|
||||
|
||||
superReactionPlayingLimit: {
|
||||
description: "Max Super Reactions to play at once",
|
||||
type: OptionType.SLIDER,
|
||||
default: 20,
|
||||
markers: [5, 10, 20, 40, 60, 80, 100],
|
||||
stickToMarkers: true,
|
||||
},
|
||||
}, {
|
||||
superReactionPlayingLimit: {
|
||||
disabled() { return this.store.unlimitedSuperReactionPlaying; },
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "SuperReactionTweaks",
|
||||
description: "Customize the limit of Super Reactions playing at once, and super react by default",
|
||||
authors: [Devs.FieryFlames, Devs.ant0n],
|
||||
patches: [
|
||||
{
|
||||
find: ",BURST_REACTION_EFFECT_PLAY",
|
||||
replacement: {
|
||||
match: /(?<=BURST_REACTION_EFFECT_PLAY:\i=>{.{50,100})(\i\(\i,\i\))>=\d+/,
|
||||
replace: "!$self.shouldPlayBurstReaction($1)"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".hasAvailableBurstCurrency)",
|
||||
replacement: {
|
||||
match: /(?<=\.useBurstReactionsExperiment.{0,20})useState\(!1\)(?=.+?(\i===\i\.EmojiIntention.REACTION))/,
|
||||
replace: "useState($self.settings.store.superReactByDefault && $1)"
|
||||
}
|
||||
}
|
||||
],
|
||||
settings,
|
||||
|
||||
shouldPlayBurstReaction(playingCount: number) {
|
||||
if (settings.store.unlimitedSuperReactionPlaying) return true;
|
||||
if (playingCount <= settings.store.superReactionPlayingLimit) return true;
|
||||
return false;
|
||||
}
|
||||
});
|
|
@ -21,8 +21,8 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { find, findLazy, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, GuildMemberStore, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
||||
import { find, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { buildSeveralUsers } from "../typingTweaks";
|
||||
|
||||
|
@ -36,10 +36,9 @@ const ThreeDots = LazyComponent(() => {
|
|||
const TypingStore = findStoreLazy("TypingStore");
|
||||
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
|
||||
|
||||
const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING);
|
||||
|
||||
function getDisplayName(guildId: string, userId: string) {
|
||||
return GuildMemberStore.getNick(guildId, userId) ?? UserStore.getUser(userId).username;
|
||||
const user = UserStore.getUser(userId);
|
||||
return GuildMemberStore.getNick(guildId, userId) ?? (user as any).globalName ?? user.username;
|
||||
}
|
||||
|
||||
function TypingIndicator({ channelId }: { channelId: string; }) {
|
||||
|
@ -51,7 +50,7 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
|
|||
const oldKeys = Object.keys(old);
|
||||
const currentKeys = Object.keys(current);
|
||||
|
||||
return oldKeys.length === currentKeys.length && JSON.stringify(oldKeys) === JSON.stringify(currentKeys);
|
||||
return oldKeys.length === currentKeys.length && currentKeys.every(key => old[key] != null);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -70,21 +69,21 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
|
|||
switch (typingUsersArray.length) {
|
||||
case 0: break;
|
||||
case 1: {
|
||||
tooltipText = Formatters.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) });
|
||||
tooltipText = i18n.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) });
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
tooltipText = Formatters.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) });
|
||||
tooltipText = i18n.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) });
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
tooltipText = Formatters.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) });
|
||||
tooltipText = i18n.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
tooltipText = Settings.plugins.TypingTweaks.enabled
|
||||
? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), count: typingUsersArray.length - 2 })
|
||||
: Formatters.Messages.SEVERAL_USERS_TYPING;
|
||||
: i18n.Messages.SEVERAL_USERS_TYPING;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -92,11 +91,10 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
|
|||
if (typingUsersArray.length > 0) {
|
||||
return (
|
||||
<Tooltip text={tooltipText!}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
{props => (
|
||||
<div
|
||||
{...props}
|
||||
style={{ marginLeft: 6, height: 16, display: "flex", alignItems: "center", zIndex: 0, cursor: "pointer" }}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<ThreeDots dotRadius={3} themed={true} />
|
||||
</div>
|
||||
|
@ -128,12 +126,22 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
patches: [
|
||||
// Normal channel
|
||||
{
|
||||
find: ".UNREAD_HIGHLIGHT",
|
||||
find: "UNREAD_IMPORTANT:",
|
||||
replacement: {
|
||||
match: /channel:(\i).{0,100}?channelEmoji,.{0,250}?\.children.{0,50}?:null/,
|
||||
replace: "$&,$self.TypingIndicator($1.id)"
|
||||
}
|
||||
},
|
||||
// Theads
|
||||
{
|
||||
// This is the thread "spine" that shows in the left
|
||||
find: "M11 9H4C2.89543 9 2 8.10457 2 7V1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1V7C0 9.20914 1.79086 11 4 11H11C11.5523 11 12 10.5523 12 10C12 9.44771 11.5523 9 11 9Z",
|
||||
replacement: {
|
||||
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
|
||||
replace: "$&,$self.TypingIndicator($1.id)"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
|
46
src/plugins/useAltSearch.ts
Normal file
46
src/plugins/useAltSearch.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "UseAlternativeSearch",
|
||||
description: "Use alternative search engine in right click menu",
|
||||
authors: [
|
||||
{
|
||||
id: 1038096782963507210n,
|
||||
name: "skyevg",
|
||||
},
|
||||
],
|
||||
patches: [
|
||||
{
|
||||
find: "https://www.google.com/search?q=",
|
||||
replacement: {
|
||||
match: /"https:\/\/www.google.com\/search\?q=".concat\(encodeURIComponent\(e\)\)/,
|
||||
replace: "Vencord.Settings.plugins.UseAlternativeSearch.source.replace(\"!QUERY!\", encodeURIComponent(e))"
|
||||
}
|
||||
}
|
||||
],
|
||||
options: {
|
||||
source: {
|
||||
description: "Search engine's url (use !QUERY! as replacement for the search term)",
|
||||
type: OptionType.STRING,
|
||||
default: "https://duckduckgo.com/?q=!QUERY!",
|
||||
}
|
||||
}
|
||||
});
|
143
src/plugins/uwuifier.ts
Normal file
143
src/plugins/uwuifier.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 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 { findOption, RequiredMessageOption } from "@api/Commands";
|
||||
import { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const endings = [
|
||||
"rawr x3",
|
||||
"OwO",
|
||||
"UwU",
|
||||
"o.O",
|
||||
"-.-",
|
||||
">w<",
|
||||
"(⑅˘꒳˘)",
|
||||
"(ꈍᴗꈍ)",
|
||||
"(˘ω˘)",
|
||||
"(U ᵕ U❁)",
|
||||
"σωσ",
|
||||
"òωó",
|
||||
"(///ˬ///✿)",
|
||||
"(U ﹏ U)",
|
||||
"( ͡o ω ͡o )",
|
||||
"ʘwʘ",
|
||||
":3",
|
||||
":3", // important enough to have twice
|
||||
"XD",
|
||||
"nyaa~~",
|
||||
"mya",
|
||||
">_<",
|
||||
"😳",
|
||||
"🥺",
|
||||
"😳😳😳",
|
||||
"rawr",
|
||||
"^^",
|
||||
"^^;;",
|
||||
"(ˆ ﻌ ˆ)♡",
|
||||
"^•ﻌ•^",
|
||||
"/(^•ω•^)",
|
||||
"(✿oωo)"
|
||||
];
|
||||
|
||||
const replacements = [
|
||||
["small", "smol"],
|
||||
["cute", "kawaii~"],
|
||||
["fluff", "floof"],
|
||||
["love", "luv"],
|
||||
["stupid", "baka"],
|
||||
["what", "nani"],
|
||||
["meow", "nya~"],
|
||||
["hello", "hewwo"],
|
||||
];
|
||||
|
||||
const settings = definePluginSettings({
|
||||
uwuEveryMessage: {
|
||||
description: "Make every single message uwuified",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
restartNeeded: false
|
||||
}
|
||||
});
|
||||
|
||||
function selectRandomElement(arr) {
|
||||
// generate a random index based on the length of the array
|
||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||
|
||||
// return the element at the randomly generated index
|
||||
return arr[randomIndex];
|
||||
}
|
||||
|
||||
|
||||
function uwuify(message: string): string {
|
||||
message = message.toLowerCase();
|
||||
// words
|
||||
for (const pair of replacements) {
|
||||
message = message.replaceAll(pair[0], pair[1]);
|
||||
}
|
||||
message = message
|
||||
.replaceAll(/([ \t\n])n/g, "$1ny") // nyaify
|
||||
.replaceAll(/[lr]/g, "w") // [lr] > w
|
||||
.replaceAll(/([ \t\n])([a-z])/g, (_, p1, p2) => Math.random() < .5 ? `${p1}${p2}-${p2}` : `${p1}${p2}`) // stutter
|
||||
.replaceAll(/([^.,!][.,!])([ \t\n])/g, (_, p1, p2) => `${p1} ${selectRandomElement(endings)}${p2}`); // endings
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// actual command declaration
|
||||
export default definePlugin({
|
||||
name: "UwUifier",
|
||||
description: "Simply uwuify commands",
|
||||
authors: [Devs.echo, Devs.skyevg, Devs.PandaNinjas],
|
||||
dependencies: ["CommandsAPI", "MessageEventsAPI"],
|
||||
settings,
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: "uwuify",
|
||||
description: "uwuifies your messages",
|
||||
options: [RequiredMessageOption],
|
||||
|
||||
execute: opts => ({
|
||||
content: uwuify(findOption(opts, "message", "")),
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
onSend(msg: MessageObject) {
|
||||
// Only run when it's enabled
|
||||
if (settings.store.uwuEveryMessage) {
|
||||
msg.content = uwuify(msg.content);
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
this.preSend = addPreSendListener((_, msg) => this.onSend(msg));
|
||||
this.preEdit = addPreEditListener((_cid, _mid, msg) =>
|
||||
this.onSend(msg)
|
||||
);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removePreSendListener(this.preSend);
|
||||
removePreEditListener(this.preEdit);
|
||||
},
|
||||
});
|
|
@ -16,11 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginNative } from "@utils/types";
|
||||
import { Button, showToast, Toasts, useState } from "@webpack/common";
|
||||
|
||||
import type { VoiceRecorder } from ".";
|
||||
import { settings } from "./settings";
|
||||
|
||||
const Native = VencordNative.pluginHelpers.VoiceMessages as PluginNative<typeof import("./native")>;
|
||||
|
||||
export const VoiceRecorderDesktop: VoiceRecorder = ({ setAudioBlob, onRecordingChange }) => {
|
||||
const [recording, setRecording] = useState(false);
|
||||
|
||||
|
@ -49,7 +52,7 @@ export const VoiceRecorderDesktop: VoiceRecorder = ({ setAudioBlob, onRecordingC
|
|||
} else {
|
||||
discordVoice.stopLocalAudioRecording(async (filePath: string) => {
|
||||
if (filePath) {
|
||||
const buf = await VencordNative.pluginHelpers.VoiceMessages.readRecording(filePath);
|
||||
const buf = await Native.readRecording(filePath);
|
||||
if (buf)
|
||||
setAudioBlob(new Blob([buf], { type: "audio/ogg; codecs=opus" }));
|
||||
else
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LazyComponent, useTimer } from "@utils/react";
|
||||
import { find } from "@webpack";
|
||||
import { useTimer } from "@utils/react";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
|
||||
import { cl } from "./utils";
|
||||
|
||||
|
@ -25,7 +25,7 @@ interface VoiceMessageProps {
|
|||
src: string;
|
||||
waveform: string;
|
||||
}
|
||||
const VoiceMessage = LazyComponent<VoiceMessageProps>(() => find(m => m.type?.toString().includes("waveform:")));
|
||||
const VoiceMessage = findComponentByCodeLazy<VoiceMessageProps>("waveform:");
|
||||
|
||||
export type VoicePreviewOptions = {
|
||||
src?: string;
|
||||
|
|
24
src/plugins/voiceMessages/native.ts
Normal file
24
src/plugins/voiceMessages/native.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { readFile } from "fs/promises";
|
||||
import { basename, normalize } from "path";
|
||||
|
||||
export async function readRecording(_, filePath: string) {
|
||||
filePath = normalize(filePath);
|
||||
const filename = basename(filePath);
|
||||
const discordBaseDirWithTrailingSlash = normalize(app.getPath("userData") + "/");
|
||||
console.log(filename, discordBaseDirWithTrailingSlash, filePath);
|
||||
if (filename !== "recording.ogg" || !filePath.startsWith(discordBaseDirWithTrailingSlash)) return null;
|
||||
|
||||
try {
|
||||
const buf = await readFile(filePath);
|
||||
return new Uint8Array(buf.buffer);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -20,8 +20,8 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { saveFile } from "@utils/web";
|
||||
import { findByProps, findLazy } from "@webpack";
|
||||
import { Clipboard } from "@webpack/common";
|
||||
import { findByProps } from "@webpack";
|
||||
import { Clipboard, ComponentDispatch } from "@webpack/common";
|
||||
|
||||
async function fetchImage(url: string) {
|
||||
const res = await fetch(url);
|
||||
|
@ -30,7 +30,6 @@ async function fetchImage(url: string) {
|
|||
return await res.blob();
|
||||
}
|
||||
|
||||
const MiniDispatcher = findLazy(m => m.emitter?._events?.INSERT_TEXT);
|
||||
|
||||
const settings = definePluginSettings({
|
||||
// This needs to be all in one setting because to enable any of these, we need to make Discord use their desktop context
|
||||
|
@ -213,7 +212,7 @@ export default definePlugin({
|
|||
|
||||
cut() {
|
||||
this.copy();
|
||||
MiniDispatcher.dispatch("INSERT_TEXT", { rawText: "" });
|
||||
ComponentDispatch.dispatch("INSERT_TEXT", { rawText: "" });
|
||||
},
|
||||
|
||||
async paste() {
|
||||
|
|
|
@ -20,14 +20,14 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { sleep } from "@utils/misc";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { LazyComponent, useForceUpdater } from "@utils/react";
|
||||
import { useForceUpdater } from "@utils/react";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
|
||||
import { CustomEmoji } from "@webpack/types";
|
||||
import { Message, ReactionEmoji, User } from "discord-types/general";
|
||||
|
||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||
|
||||
const queue = new Queue();
|
||||
|
|
|
@ -38,6 +38,8 @@ export const enum IpcEvents {
|
|||
BUILD = "VencordBuild",
|
||||
OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor",
|
||||
|
||||
GET_PLUGIN_IPC_METHOD_MAP = "VencordGetPluginIpcMethodMap",
|
||||
|
||||
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
|
||||
VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording",
|
||||
}
|
||||
|
|
|
@ -267,6 +267,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Dziurwa",
|
||||
id: 1001086404203389018n
|
||||
},
|
||||
F53: {
|
||||
name: "F53",
|
||||
id: 280411966126948353n
|
||||
},
|
||||
AutumnVN: {
|
||||
name: "AutumnVN",
|
||||
id: 393694671383166998n
|
||||
|
@ -379,6 +383,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "ProffDea",
|
||||
id: 609329952180928513n
|
||||
},
|
||||
ant0n: {
|
||||
name: "ant0n",
|
||||
id: 145224646868860928n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
@ -43,7 +43,6 @@ for (const method of [
|
|||
"construct",
|
||||
"defineProperty",
|
||||
"deleteProperty",
|
||||
"get",
|
||||
"getOwnPropertyDescriptor",
|
||||
"getPrototypeOf",
|
||||
"has",
|
||||
|
@ -86,7 +85,11 @@ handler.getOwnPropertyDescriptor = (target, p) => {
|
|||
* Note that the example below exists already as an api, see {@link findByPropsLazy}
|
||||
* @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah);
|
||||
*/
|
||||
export function proxyLazy<T>(factory: () => T, attempts = 5): T {
|
||||
export function proxyLazy<T>(factory: () => T, attempts = 5, isChild = false): T {
|
||||
let isSameTick = true;
|
||||
if (!isChild)
|
||||
setTimeout(() => isSameTick = false, 0);
|
||||
|
||||
let tries = 0;
|
||||
const proxyDummy = Object.assign(function () { }, {
|
||||
[kCACHE]: void 0 as T | undefined,
|
||||
|
@ -100,5 +103,21 @@ export function proxyLazy<T>(factory: () => T, attempts = 5): T {
|
|||
}
|
||||
});
|
||||
|
||||
return new Proxy(proxyDummy, handler) as any;
|
||||
return new Proxy(proxyDummy, {
|
||||
...handler,
|
||||
get(target, p, receiver) {
|
||||
// if we're still in the same tick, it means the lazy was immediately used.
|
||||
// thus, we lazy proxy the get access to make things like destructuring work as expected
|
||||
// meow here will also be a lazy
|
||||
// `const { meow } = findByPropsLazy("meow");`
|
||||
if (!isChild && isSameTick)
|
||||
return proxyLazy(
|
||||
() => Reflect.get(target[kGET](), p, receiver),
|
||||
attempts,
|
||||
true
|
||||
);
|
||||
|
||||
return Reflect.get(target[kGET](), p, receiver);
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
|
|
23
src/utils/lazyReact.tsx
Normal file
23
src/utils/lazyReact.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { makeLazy } from "./lazy";
|
||||
|
||||
const NoopComponent = () => null;
|
||||
|
||||
/**
|
||||
* A lazy component. The factory method is called on first render.
|
||||
* @param factory Function returning a Component
|
||||
* @param attempts How many times to try to get the component before giving up
|
||||
* @returns Result of factory function
|
||||
*/
|
||||
export function LazyComponent<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) {
|
||||
const get = makeLazy(factory, attempts);
|
||||
return (props: T) => {
|
||||
const Component = get() ?? NoopComponent;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { filters, findByProps, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
||||
import { findByProps, findByPropsLazy } from "@webpack";
|
||||
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
|
||||
|
||||
import { LazyComponent } from "./react";
|
||||
|
@ -49,13 +49,7 @@ export interface ModalOptions {
|
|||
|
||||
type RenderFunction = (props: ModalProps) => ReactNode;
|
||||
|
||||
export const Modals = mapMangledModuleLazy(".closeWithCircleBackground", {
|
||||
ModalRoot: filters.byCode(".root"),
|
||||
ModalHeader: filters.byCode(".header"),
|
||||
ModalContent: filters.byCode(".content"),
|
||||
ModalFooter: filters.byCode(".footerSeparator"),
|
||||
ModalCloseButton: filters.byCode(".closeWithCircleBackground"),
|
||||
}) as {
|
||||
export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as {
|
||||
ModalRoot: ComponentType<PropsWithChildren<{
|
||||
transitionState: ModalTransitionState;
|
||||
size?: ModalSize;
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
import { React, useEffect, useMemo, useReducer, useState } from "@webpack/common";
|
||||
|
||||
import { makeLazy } from "./lazy";
|
||||
import { checkIntersecting } from "./misc";
|
||||
|
||||
export * from "./lazyReact";
|
||||
|
||||
export const NoopComponent = () => null;
|
||||
|
||||
/**
|
||||
|
@ -77,7 +78,6 @@ interface AwaiterOpts<T> {
|
|||
* @param fallbackValue The fallback value that will be used until the promise resolved
|
||||
* @returns [value, error, isPending]
|
||||
*/
|
||||
|
||||
export function useAwaiter<T>(factory: () => Promise<T>): AwaiterRes<T | null>;
|
||||
export function useAwaiter<T>(factory: () => Promise<T>, providedOpts: AwaiterOpts<T>): AwaiterRes<T>;
|
||||
export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterOpts<T | null>): AwaiterRes<T | null> {
|
||||
|
@ -113,31 +113,16 @@ export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterO
|
|||
|
||||
return [state.value, state.error, state.pending];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that can be used to force rerender react components
|
||||
*/
|
||||
|
||||
export function useForceUpdater(): () => void;
|
||||
export function useForceUpdater(withDep: true): [unknown, () => void];
|
||||
export function useForceUpdater(withDep?: true) {
|
||||
const r = useReducer(x => x + 1, 0);
|
||||
return withDep ? r : r[1];
|
||||
}
|
||||
/**
|
||||
* A lazy component. The factory method is called on first render. For example useful
|
||||
* for const Component = LazyComponent(() => findByDisplayName("...").default)
|
||||
* @param factory Function returning a Component
|
||||
* @param attempts How many times to try to get the component before giving up
|
||||
* @returns Result of factory function
|
||||
*/
|
||||
|
||||
export function LazyComponent<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) {
|
||||
const get = makeLazy(factory, attempts);
|
||||
return (props: T) => {
|
||||
const Component = get() ?? NoopComponent;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
}
|
||||
|
||||
interface TimerOpts {
|
||||
interval?: number;
|
||||
|
|
|
@ -41,6 +41,8 @@ export interface Patch {
|
|||
all?: boolean;
|
||||
/** Do not warn if this patch did no changes */
|
||||
noWarn?: boolean;
|
||||
/** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */
|
||||
group?: boolean;
|
||||
predicate?(): boolean;
|
||||
}
|
||||
|
||||
|
@ -80,6 +82,11 @@ export interface PluginDef {
|
|||
* Whether this plugin should be enabled by default, but can be disabled
|
||||
*/
|
||||
enabledByDefault?: boolean;
|
||||
/**
|
||||
* When to call the start() method
|
||||
* @default StartAt.WebpackReady
|
||||
*/
|
||||
startAt?: StartAt,
|
||||
/**
|
||||
* Optionally provide settings that the user can configure in the Plugins tab of settings.
|
||||
* @deprecated Use `settings` instead
|
||||
|
@ -117,6 +124,15 @@ export interface PluginDef {
|
|||
tags?: string[];
|
||||
}
|
||||
|
||||
export const enum StartAt {
|
||||
/** Right away, as soon as Vencord initialised */
|
||||
Init = "Init",
|
||||
/** On the DOMContentLoaded event, so once the document is ready */
|
||||
DOMContentLoaded = "DOMContentLoaded",
|
||||
/** Once Discord's core webpack modules have finished loading, so as soon as things like react and flux are available */
|
||||
WebpackReady = "WebpackReady"
|
||||
}
|
||||
|
||||
export const enum OptionType {
|
||||
STRING,
|
||||
NUMBER,
|
||||
|
@ -307,3 +323,10 @@ export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon
|
|||
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
|
||||
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
|
||||
export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon;
|
||||
|
||||
export type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = {
|
||||
[key in keyof PluginExports]:
|
||||
PluginExports[key] extends (event: Electron.IpcMainInvokeEvent, ...args: infer Args) => infer Return
|
||||
? (...args: Args) => Return extends Promise<any> ? Return : Promise<Return>
|
||||
: never;
|
||||
};
|
||||
|
|
|
@ -17,16 +17,12 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { filters, mapMangledModuleLazy, waitFor } from "../webpack";
|
||||
import { findByPropsLazy, waitFor } from "../webpack";
|
||||
import type * as t from "./types/menu";
|
||||
|
||||
export let Menu = {} as t.Menu;
|
||||
|
||||
waitFor(["MenuItem", "MenuSliderControl"], m => Menu = m);
|
||||
|
||||
export const ContextMenu: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN"', {
|
||||
open: filters.byCode("stopPropagation"),
|
||||
openLazy: m => m.toString().length < 50,
|
||||
close: filters.byCode("CONTEXT_MENU_CLOSE")
|
||||
});
|
||||
export const ContextMenuApi: t.ContextMenuApi = findByPropsLazy("closeContextMenu", "openContextMenu");
|
||||
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import type * as Stores from "discord-types/stores";
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { filters, findByProps, findByPropsLazy, mapMangledModuleLazy } from "../webpack";
|
||||
import { findByPropsLazy } from "../webpack";
|
||||
import { waitForStore } from "./internal";
|
||||
import * as t from "./types/stores";
|
||||
|
||||
|
@ -63,10 +62,6 @@ export let EmojiStore: t.EmojiStore;
|
|||
export let WindowStore: t.WindowStore;
|
||||
export let DraftStore: t.DraftStore;
|
||||
|
||||
export const MaskedLinkStore = mapMangledModuleLazy('"MaskedLinkStore"', {
|
||||
openUntrustedLink: filters.byCode(".apply(this,arguments)")
|
||||
});
|
||||
|
||||
/**
|
||||
* React hook that returns stateful data for one or more stores
|
||||
* You might need a custom comparator (4th argument) if your store data is an object
|
||||
|
@ -78,13 +73,15 @@ export const MaskedLinkStore = mapMangledModuleLazy('"MaskedLinkStore"', {
|
|||
*
|
||||
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
|
||||
*/
|
||||
export const useStateFromStores: <T>(
|
||||
export const { useStateFromStores }: {
|
||||
useStateFromStores: <T>(
|
||||
stores: t.FluxStore[],
|
||||
mapper: () => T,
|
||||
idk?: any,
|
||||
isEqual?: (old: T, newer: T) => boolean
|
||||
) => T
|
||||
= proxyLazy(() => findByProps("useStateFromStores").useStateFromStores);
|
||||
) => T;
|
||||
}
|
||||
= findByPropsLazy("useStateFromStores");
|
||||
|
||||
waitForStore("DraftStore", s => DraftStore = s);
|
||||
waitForStore("UserStore", s => UserStore = s);
|
||||
|
|
6
src/webpack/common/types/menu.d.ts
vendored
6
src/webpack/common/types/menu.d.ts
vendored
|
@ -75,14 +75,14 @@ export interface Menu {
|
|||
}
|
||||
|
||||
export interface ContextMenuApi {
|
||||
close(): void;
|
||||
open(
|
||||
closeContextMenu(): void;
|
||||
openContextMenu(
|
||||
event: UIEvent,
|
||||
render?: Menu["Menu"],
|
||||
options?: { enableSpellCheck?: boolean; },
|
||||
renderLazy?: () => Promise<Menu["Menu"]>
|
||||
): void;
|
||||
openLazy(
|
||||
openContextMenuLazy(
|
||||
event: UIEvent,
|
||||
renderLazy?: () => Promise<Menu["Menu"]>,
|
||||
options?: { enableSpellCheck?: boolean; }
|
||||
|
|
23
src/webpack/common/types/utils.d.ts
vendored
23
src/webpack/common/types/utils.d.ts
vendored
|
@ -159,5 +159,26 @@ export interface i18n {
|
|||
|
||||
loadPromise: Promise<void>;
|
||||
|
||||
Messages: Record<i18nMessages, string>;
|
||||
Messages: Record<i18nMessages, any>;
|
||||
}
|
||||
|
||||
export interface Clipboard {
|
||||
copy(text: string): void;
|
||||
SUPPORTS_COPY: boolean;
|
||||
}
|
||||
|
||||
export interface NavigationRouter {
|
||||
back(): void;
|
||||
forward(): void;
|
||||
hasNavigated(): boolean;
|
||||
getHistory(): {
|
||||
action: string;
|
||||
length: 50;
|
||||
[key: string]: any;
|
||||
};
|
||||
transitionTo(path: string, ...args: unknown[]): void;
|
||||
transitionToGuild(guildId: string, ...args: unknown[]): void;
|
||||
replaceWith(...args: unknown[]): void;
|
||||
getLastRouteChangeSource(): any;
|
||||
getLastRouteChangeSourceLocationStack(): any;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { proxyLazy } from "@utils/lazy";
|
|||
import type { Channel, User } from "discord-types/general";
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { _resolveReady, filters, find, findByPropsLazy, findLazy, mapMangledModuleLazy, waitFor } from "../webpack";
|
||||
import { _resolveReady, find, findByPropsLazy, findLazy, waitFor } from "../webpack";
|
||||
import type * as t from "./types/utils";
|
||||
|
||||
export let FluxDispatcher: t.FluxDispatcher;
|
||||
|
@ -31,7 +31,7 @@ waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m
|
|||
export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get");
|
||||
export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear");
|
||||
|
||||
export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight");
|
||||
export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage");
|
||||
|
||||
export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep");
|
||||
|
||||
|
@ -102,17 +102,9 @@ export const ApplicationAssetUtils = findByPropsLazy("fetchAssetIds", "getAssetI
|
|||
fetchAssetIds: (applicationId: string, e: string[]) => Promise<string[]>;
|
||||
};
|
||||
|
||||
export const Clipboard = mapMangledModuleLazy('document.queryCommandEnabled("copy")||document.queryCommandSupported("copy")', {
|
||||
copy: filters.byCode(".copy("),
|
||||
SUPPORTS_COPY: x => typeof x === "boolean",
|
||||
});
|
||||
export const Clipboard: t.Clipboard = findByPropsLazy("SUPPORTS_COPY", "copy");
|
||||
|
||||
export const NavigationRouter = mapMangledModuleLazy("transitionToGuild - ", {
|
||||
transitionTo: filters.byCode("transitionTo -"),
|
||||
transitionToGuild: filters.byCode("transitionToGuild -"),
|
||||
goBack: filters.byCode("goBack()"),
|
||||
goForward: filters.byCode("goForward()"),
|
||||
});
|
||||
export const NavigationRouter: t.NavigationRouter = findByPropsLazy("transitionTo", "replaceWith", "transitionToGuild");
|
||||
|
||||
waitFor(["dispatch", "subscribe"], m => {
|
||||
FluxDispatcher = m;
|
||||
|
|
|
@ -119,12 +119,9 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
|
|||
// Additionally, `[actual newline]` is one less char than "\n", so if Discord
|
||||
// ever targets newer browsers, the minifier could potentially use this trick and
|
||||
// cause issues.
|
||||
let code: string = mod.toString().replaceAll("\n", "");
|
||||
// a very small minority of modules use function() instead of arrow functions,
|
||||
// but, unnamed toplevel functions aren't valid. However 0, function() makes it a statement
|
||||
if (code.startsWith("function(")) {
|
||||
code = "0," + code;
|
||||
}
|
||||
//
|
||||
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
|
||||
let code: string = "0," + mod.toString().replaceAll("\n", "");
|
||||
const originalMod = mod;
|
||||
const patchedBy = new Set();
|
||||
|
||||
|
@ -170,19 +167,10 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
|
|||
if (filter(exports)) {
|
||||
subscriptions.delete(filter);
|
||||
callback(exports, numberId);
|
||||
} else if (typeof exports === "object") {
|
||||
if (exports.default && filter(exports.default)) {
|
||||
} else if (exports.default && filter(exports.default)) {
|
||||
subscriptions.delete(filter);
|
||||
callback(exports.default, numberId);
|
||||
}
|
||||
|
||||
for (const nested in exports) if (nested.length <= 3) {
|
||||
if (exports[nested] && filter(exports[nested])) {
|
||||
subscriptions.delete(filter);
|
||||
callback(exports[nested], numberId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error while firing callback for webpack chunk", err);
|
||||
}
|
||||
|
@ -191,10 +179,8 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
|
|||
|
||||
// for some reason throws some error on which calling .toString() leads to infinite recursion
|
||||
// when you force load all chunks???
|
||||
try {
|
||||
factory.toString = () => mod.toString();
|
||||
factory.original = originalMod;
|
||||
} catch { }
|
||||
|
||||
for (let i = 0; i < patches.length; i++) {
|
||||
const patch = patches[i];
|
||||
|
@ -204,6 +190,9 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
|
|||
if (code.includes(patch.find)) {
|
||||
patchedBy.add(patch.plugin);
|
||||
|
||||
const previousMod = mod;
|
||||
const previousCode = code;
|
||||
|
||||
// we change all patch.replacement to array in plugins/index
|
||||
for (const replacement of patch.replacement as PatchReplacement[]) {
|
||||
if (replacement.predicate && !replacement.predicate()) continue;
|
||||
|
@ -214,12 +203,21 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
|
|||
|
||||
try {
|
||||
const newCode = executePatch(replacement.match, replacement.replace as string);
|
||||
if (newCode === code && !patch.noWarn) {
|
||||
(window.explosivePlugins ??= new Set<string>()).add(patch.plugin);
|
||||
if (newCode === code) {
|
||||
if (!patch.noWarn) {
|
||||
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
|
||||
if (IS_DEV) {
|
||||
logger.debug("Function Source:\n", code);
|
||||
}
|
||||
}
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
|
||||
code = previousCode;
|
||||
mod = previousMod;
|
||||
patchedBy.delete(patch.plugin);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
code = newCode;
|
||||
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
|
||||
|
@ -259,9 +257,17 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
|
|||
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
|
||||
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
|
||||
}
|
||||
|
||||
patchedBy.delete(patch.plugin);
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||
code = previousCode;
|
||||
mod = previousMod;
|
||||
break;
|
||||
}
|
||||
|
||||
code = lastCode;
|
||||
mod = lastMod;
|
||||
patchedBy.delete(patch.plugin);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { LazyComponent } from "@utils/lazyReact";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import type { WebpackInstance } from "discord-types/other";
|
||||
|
||||
|
@ -51,7 +52,18 @@ export const filters = {
|
|||
return true;
|
||||
},
|
||||
byStoreName: (name: string): FilterFn => m =>
|
||||
m.constructor?.displayName === name
|
||||
m.constructor?.displayName === name,
|
||||
|
||||
componentByCode: (...code: string[]): FilterFn => {
|
||||
const filter = filters.byCode(...code);
|
||||
return m => {
|
||||
if (filter(m)) return true;
|
||||
if (!m.$$typeof) return false;
|
||||
if (m.type) return filter(m.type); // memos
|
||||
if (m.render) return filter(m.render); // forwardRefs
|
||||
return false;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const subscriptions = new Map<FilterFn, CallbackFn>();
|
||||
|
@ -67,44 +79,6 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
|
|||
if (!wreq) return false;
|
||||
|
||||
cache = wreq.c;
|
||||
|
||||
for (const id in cache) {
|
||||
const { exports } = cache[id];
|
||||
if (!exports) continue;
|
||||
|
||||
const numberId = Number(id);
|
||||
|
||||
for (const callback of listeners) {
|
||||
try {
|
||||
callback(exports, numberId);
|
||||
} catch (err) {
|
||||
logger.error("Error in webpack listener", err);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [filter, callback] of subscriptions) {
|
||||
try {
|
||||
if (filter(exports)) {
|
||||
subscriptions.delete(filter);
|
||||
callback(exports, numberId);
|
||||
} else if (typeof exports === "object") {
|
||||
if (exports.default && filter(exports.default)) {
|
||||
subscriptions.delete(filter);
|
||||
callback(exports.default, numberId);
|
||||
}
|
||||
|
||||
for (const nested in exports) if (nested.length <= 3) {
|
||||
if (exports[nested] && filter(exports[nested])) {
|
||||
subscriptions.delete(filter);
|
||||
callback(exports[nested], numberId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error while firing callback for webpack chunk", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -140,20 +114,10 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
|
|||
return isWaitFor ? [mod.exports, Number(key)] : mod.exports;
|
||||
}
|
||||
|
||||
if (typeof mod.exports !== "object") continue;
|
||||
|
||||
if (mod.exports.default && filter(mod.exports.default)) {
|
||||
const found = mod.exports.default;
|
||||
return isWaitFor ? [found, Number(key)] : found;
|
||||
}
|
||||
|
||||
// the length check makes search about 20% faster
|
||||
for (const nestedMod in mod.exports) if (nestedMod.length <= 3) {
|
||||
const nested = mod.exports[nestedMod];
|
||||
if (nested && filter(nested)) {
|
||||
return isWaitFor ? [nested, Number(key)] : nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isIndirect) {
|
||||
|
@ -181,15 +145,9 @@ export function findAll(filter: FilterFn) {
|
|||
|
||||
if (filter(mod.exports))
|
||||
ret.push(mod.exports);
|
||||
else if (typeof mod.exports !== "object")
|
||||
continue;
|
||||
|
||||
if (mod.exports.default && filter(mod.exports.default))
|
||||
ret.push(mod.exports.default);
|
||||
else for (const nestedMod in mod.exports) if (nestedMod.length <= 3) {
|
||||
const nested = mod.exports[nestedMod];
|
||||
if (nested && filter(nested)) ret.push(nested);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -239,26 +197,12 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns
|
|||
break;
|
||||
}
|
||||
|
||||
if (typeof mod.exports !== "object")
|
||||
continue;
|
||||
|
||||
if (mod.exports.default && filter(mod.exports.default)) {
|
||||
results[j] = mod.exports.default;
|
||||
filters[j] = undefined;
|
||||
if (++found === length) break outer;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const nestedMod in mod.exports)
|
||||
if (nestedMod.length <= 3) {
|
||||
const nested = mod.exports[nestedMod];
|
||||
if (nested && filter(nested)) {
|
||||
results[j] = nested;
|
||||
filters[j] = undefined;
|
||||
if (++found === length) break outer;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,47 +244,6 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId(
|
|||
return null;
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module)
|
||||
* then maps it into an easily usable module via the specified mappers
|
||||
* @param code Code snippet
|
||||
* @param mappers Mappers to create the non mangled exports
|
||||
* @returns Unmangled exports as specified in mappers
|
||||
*
|
||||
* @example mapMangledModule("headerIdIsManaged:", {
|
||||
* openModal: filters.byCode("headerIdIsManaged:"),
|
||||
* closeModal: filters.byCode("key==")
|
||||
* })
|
||||
*/
|
||||
export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> {
|
||||
const exports = {} as Record<S, any>;
|
||||
|
||||
const id = findModuleId(code);
|
||||
if (id === null)
|
||||
return exports;
|
||||
|
||||
const mod = wreq(id);
|
||||
outer:
|
||||
for (const key in mod) {
|
||||
const member = mod[key];
|
||||
for (const newName in mappers) {
|
||||
// if the current mapper matches this module
|
||||
if (mappers[newName](member)) {
|
||||
exports[newName] = member;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
return exports;
|
||||
});
|
||||
|
||||
/**
|
||||
* Same as {@link mapMangledModule} but lazy
|
||||
*/
|
||||
export function mapMangledModuleLazy<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> {
|
||||
return proxyLazy(() => mapMangledModule(code, mappers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first module that has the specified properties
|
||||
*/
|
||||
|
@ -386,12 +289,43 @@ export function findStore(name: string) {
|
|||
}
|
||||
|
||||
/**
|
||||
* findByDisplayName but lazy
|
||||
* findStore but lazy
|
||||
*/
|
||||
export function findStoreLazy(name: string) {
|
||||
return proxyLazy(() => findStore(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the component which includes all the given code. Checks for plain components, memos and forwardRefs
|
||||
*/
|
||||
export function findComponentByCode(...code: string[]) {
|
||||
const res = find(filters.componentByCode(...code), { isIndirect: true });
|
||||
if (!res)
|
||||
handleModuleNotFound("findComponentByCode", ...code);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first component that matches the filter, lazily.
|
||||
*/
|
||||
export function findComponentLazy<T extends object = any>(filter: FilterFn) {
|
||||
return LazyComponent<T>(() => find(filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first component that includes all the given code, lazily
|
||||
*/
|
||||
export function findComponentByCodeLazy<T extends object = any>(...code: string[]) {
|
||||
return LazyComponent<T>(() => findComponentByCode(...code));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first component that is exported by the first prop name, lazily
|
||||
*/
|
||||
export function findExportedComponentLazy<T extends object = any>(...props: string[]) {
|
||||
return LazyComponent<T>(() => findByProps(...props)?.[props[0]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a module that matches the provided filter to be registered,
|
||||
* then call the callback with the module as the first argument
|
||||
|
|
Loading…
Reference in a new issue