Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ea11f2244f | ||
|
96126fa39f | ||
|
9bd82943e3 | ||
|
5edc94062c | ||
|
394d2060eb | ||
|
119b628f33 | ||
|
32f2043193 | ||
|
04d2dd26c4 | ||
|
86e94343cc | ||
|
dd44ac1ad2 | ||
|
370b3d366d | ||
|
a67c7f841d | ||
|
2c9793202d | ||
|
a257926609 | ||
|
77659be4f0 | ||
|
37b9a62460 | ||
|
7f73e13364 | ||
|
fcf2bdda70 | ||
|
fa9da2d693 | ||
|
44b21394b3 | ||
|
27fffc8bc3 | ||
|
098da8c3fd | ||
|
9cf88d4232 | ||
|
dd61b0c999 | ||
|
5dc0d06be1 | ||
|
9de6c2d4ff | ||
|
e37f62ac0a | ||
|
56a9d79f85 | ||
|
9d78233afa | ||
|
9af2ec65ae | ||
|
584885acf5 | ||
|
18fdc33ee7 | ||
|
522fdcd15d | ||
|
af135b9245 | ||
|
4b958d17fd | ||
|
bfb48b4faf | ||
|
9dd8e72245 | ||
|
aae790f1c1 | ||
|
7f17e70697 | ||
|
b3311c6f12 | ||
|
bc09225258 | ||
|
9ce923d4d7 | ||
|
7845af0802 | ||
|
89672882b9 | ||
|
e05c630a54 | ||
|
38834ef7ac | ||
|
98d49af728 | ||
|
0afe319141 | ||
|
a9e67e2955 | ||
|
1676956f61 | ||
|
8692109bc5 | ||
|
0847f205b8 | ||
|
2f94e167c4 | ||
|
589c070773 | ||
|
8567ff6239 | ||
|
e4701769a5 | ||
|
25f101602d | ||
|
85bfa1e719 | ||
|
b48998d485 | ||
|
6d605050e1 | ||
|
07c4a097e0 | ||
|
c1de41436a | ||
|
64c6f5740f | ||
|
03523446c1 | ||
|
25dc25c707 | ||
|
8ac8048845 |
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -14,7 +14,8 @@ body:
|
||||
DO NOT USE THIS FORM, unless
|
||||
- you are a vencord contributor
|
||||
- you were given explicit permission to use this form by a moderator in our support server
|
||||
- you are filing a security related report
|
||||
|
||||
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
|
||||
|
||||
- type: input
|
||||
id: discord
|
||||
|
@ -28,6 +28,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">
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.3",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
@ -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;
|
||||
|
@ -289,7 +289,7 @@ function runTime(token: string) {
|
||||
setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000);
|
||||
}, 1000));
|
||||
} catch (e) {
|
||||
console.error("[PUP_DEBUG]", "A fatal error occured");
|
||||
console.error("[PUP_DEBUG]", "A fatal error occurred");
|
||||
console.error("[PUP_DEBUG]", e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -69,7 +69,7 @@ export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback)
|
||||
* Remove a context menu patch
|
||||
* @param navId The navId(s) for the context menu(s) to remove the patch
|
||||
* @param patch The patch to be removed
|
||||
* @returns Wheter the patch was sucessfully removed from the context menu(s)
|
||||
* @returns Whether the patch was successfully removed from the context menu(s)
|
||||
*/
|
||||
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
|
||||
const navIds = Array.isArray(navId) ? navId : [navId as string];
|
||||
@ -82,7 +82,7 @@ export function removeContextMenuPatch<T extends string | Array<string>>(navId:
|
||||
/**
|
||||
* Remove a global context menu patch
|
||||
* @param patch The patch to be removed
|
||||
* @returns Wheter the patch was sucessfully removed
|
||||
* @returns Whether the patch was successfully removed
|
||||
*/
|
||||
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
|
||||
return globalPatches.delete(patch);
|
||||
|
@ -46,7 +46,7 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
||||
if (code === "ENOENT")
|
||||
var err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
else {
|
||||
var err = `An error occured while running \`${cmd}\`:\n`;
|
||||
var err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
err += stderr || `Code \`${code}\`. See the console for more info`;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
// #region OpenInApp
|
||||
// These links don't support CORS, so this has to be native
|
||||
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||
const mappings = PluginIpcMappings[plugin] = {};
|
||||
|
||||
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();
|
||||
});
|
||||
for (const [methodName, method] of entries) {
|
||||
const key = `VencordPluginNative_${plugin}_${methodName}`;
|
||||
ipcMain.handle(key, method);
|
||||
mappings[methodName] = key;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -49,7 +49,9 @@ async function getRepo() {
|
||||
async function calculateGitChanges() {
|
||||
await git("fetch");
|
||||
|
||||
const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s");
|
||||
const branch = await git("branch", "--show-current");
|
||||
|
||||
const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s");
|
||||
|
||||
const commits = res.stdout.trim();
|
||||
return commits ? commits.split("\n").map(line => {
|
||||
|
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;
|
||||
|
@ -27,8 +27,8 @@ export default definePlugin({
|
||||
{
|
||||
find: '"Message Username"',
|
||||
replacement: {
|
||||
match: /currentUserIsPremium:.{0,70}{children:\i(?=}\))/,
|
||||
replace: "$&.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))"
|
||||
match: /\.Messages\.GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE.+?}\),\i(?=\])/,
|
||||
replace: "$&,...Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -21,16 +21,30 @@ import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "AlwaysAnimate",
|
||||
description: "Animates anything that can be animated, besides status emojis.",
|
||||
description: "Animates anything that can be animated",
|
||||
authors: [Devs.FieryFlames],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".canAnimate",
|
||||
find: "canAnimate:",
|
||||
all: true,
|
||||
// Some modules match the find but the replacement is returned untouched
|
||||
noWarn: true,
|
||||
replacement: {
|
||||
match: /\.canAnimate\b/g,
|
||||
replace: ".canAnimate || true"
|
||||
match: /canAnimate:.+?(?=([,}].*?\)))/g,
|
||||
replace: (m, rest) => {
|
||||
const destructuringMatch = rest.match(/}=.+/);
|
||||
if (destructuringMatch == null) return "canAnimate:!0";
|
||||
return m;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Status emojis
|
||||
find: ".Messages.GUILD_OWNER,",
|
||||
replacement: {
|
||||
match: /(?<=\.activityEmoji,.+?animate:)\i/,
|
||||
replace: "!0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -16,56 +16,44 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { i18n, React, useStateFromStores } from "@webpack/common";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { find, findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { useStateFromStores } from "@webpack/common";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-bf-");
|
||||
const classes = findByPropsLazy("sidebar", "guilds");
|
||||
import { ExpandedGuildFolderStore, settings } from ".";
|
||||
|
||||
const Animations = findByPropsLazy("a", "animated", "useTransition");
|
||||
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
|
||||
const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
|
||||
const Animations = findByPropsLazy("a", "animated", "useTransition");
|
||||
const GuildsBar = LazyComponent(() => find(m => m.type?.toString().includes('("guildsnav")')));
|
||||
|
||||
function Guilds(props: {
|
||||
className: string;
|
||||
bfGuildFolders: any[];
|
||||
}) {
|
||||
// @ts-expect-error
|
||||
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props);
|
||||
|
||||
// TODO: Make this better
|
||||
const scrollerProps = res.props.children?.props?.children?.props?.children?.[1]?.props;
|
||||
if (scrollerProps?.children) {
|
||||
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
|
||||
if (servers) scrollerProps.children = servers;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export default ErrorBoundary.wrap(() => {
|
||||
export default ErrorBoundary.wrap(guildsBarProps => {
|
||||
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
|
||||
const fullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
|
||||
|
||||
const guilds = document.querySelector(`.${classes.guilds}`);
|
||||
|
||||
const visible = !!expandedFolders.size;
|
||||
const className = cl("folder-sidebar", { fullscreen });
|
||||
const isFullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
|
||||
|
||||
const Sidebar = (
|
||||
<Guilds
|
||||
className={classes.guilds}
|
||||
bfGuildFolders={Array.from(expandedFolders)}
|
||||
<GuildsBar
|
||||
{...guildsBarProps}
|
||||
isBetterFolders={true}
|
||||
betterFoldersExpandedIds={expandedFolders}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!guilds || !Settings.plugins.BetterFolders.sidebarAnim)
|
||||
const visible = !!expandedFolders.size;
|
||||
const guilds = document.querySelector(guildsBarProps.className.split(" ").map(c => `.${c}`).join(""));
|
||||
|
||||
// We need to display none if we are in fullscreen. Yes this seems horrible doing with css, but it's literally how Discord does it.
|
||||
// Also display flex otherwise to fix scrolling
|
||||
const barStyle = {
|
||||
display: isFullscreen ? "none" : "flex",
|
||||
} as CSSProperties;
|
||||
|
||||
if (!guilds || !settings.store.sidebarAnim) {
|
||||
return visible
|
||||
? <div className={className}>{Sidebar}</div>
|
||||
? <div style={barStyle}>{Sidebar}</div>
|
||||
: null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Animations.Transition
|
||||
@ -75,11 +63,13 @@ export default ErrorBoundary.wrap(() => {
|
||||
leave={{ width: 0 }}
|
||||
config={{ duration: 200 }}
|
||||
>
|
||||
{(style, show) => show && (
|
||||
<Animations.animated.div style={style} className={className}>
|
||||
{Sidebar}
|
||||
</Animations.animated.div>
|
||||
)}
|
||||
{(animationStyle, show) =>
|
||||
show && (
|
||||
<Animations.animated.div style={{ ...animationStyle, ...barStyle }}>
|
||||
{Sidebar}
|
||||
</Animations.animated.div>
|
||||
)
|
||||
}
|
||||
</Animations.Transition>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
@ -1,17 +0,0 @@
|
||||
.vc-bf-folder-sidebar [class*="wrapper-"] > [class*="listItem-"]:first-of-type,
|
||||
.vc-bf-folder-sidebar [class*="unreadMentionsIndicator"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vc-bf-folder-sidebar [class*="expandedFolderBackground-"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.vc-bf-folder-sidebar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.vc-bf-fullscreen {
|
||||
width: 0 !important;
|
||||
visibility: hidden;
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./betterFolders.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
|
||||
import FolderSideBar from "./FolderSideBar";
|
||||
|
||||
const GuildsTree = findLazy(m => m.prototype?.convertToFolder);
|
||||
const GuildFolderStore = findStoreLazy("SortedGuildStore");
|
||||
const ExpandedFolderStore = findStoreLazy("ExpandedGuildFolderStore");
|
||||
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
sidebar: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Display servers from folder on dedicated sidebar",
|
||||
default: true,
|
||||
},
|
||||
sidebarAnim: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Animate opening the folder sidebar",
|
||||
default: true,
|
||||
},
|
||||
closeAllFolders: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Close all folders when selecting a server not in a folder",
|
||||
default: false,
|
||||
},
|
||||
closeAllHomeButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Close all folders when clicking on the home button",
|
||||
default: false,
|
||||
},
|
||||
closeOthers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Close other folders when opening a folder",
|
||||
default: false,
|
||||
},
|
||||
forceOpen: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Force a folder to open when switching to a server of that folder",
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterFolders",
|
||||
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
|
||||
authors: [Devs.juby, Devs.AutumnVN],
|
||||
patches: [
|
||||
{
|
||||
find: '("guildsnav")',
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: [
|
||||
{
|
||||
match: /(\i)\(\){return \i\(\(0,\i\.jsx\)\("div",{className:\i\(\)\.guildSeparator}\)\)}/,
|
||||
replace: "$&$self.Separator=$1;"
|
||||
},
|
||||
|
||||
// Folder component patch
|
||||
{
|
||||
match: /\i\(\(function\(\i,\i,\i\){var \i=\i\.key;return.+\(\i\)},\i\)}\)\)/,
|
||||
replace: "arguments[0].bfHideServers?null:$&"
|
||||
},
|
||||
|
||||
// BEGIN Guilds component patch
|
||||
{
|
||||
match: /(\i)\.themeOverride,(.{15,25}\(function\(\){var \i=)(\i\.\i\.getGuildsTree\(\))/,
|
||||
replace: "$1.themeOverride,bfPatch=$1.bfGuildFolders,$2bfPatch?$self.getGuildsTree(bfPatch,$3):$3"
|
||||
},
|
||||
{
|
||||
match: /return(\(0,\i\.jsx\))(\(\i,{)(folderNode:\i,setNodeRef:\i\.setNodeRef,draggable:!0,.+},\i\.id\));case/,
|
||||
replace: "var bfHideServers=typeof bfPatch==='undefined',folder=$1$2bfHideServers,$3;return !bfHideServers&&arguments[1]?[$1($self.Separator,{}),folder]:folder;case"
|
||||
},
|
||||
// END
|
||||
|
||||
{
|
||||
match: /\("guildsnav"\);return\(0,\i\.jsx\)\(.{1,6},{navigator:\i,children:\(0,\i\.jsx\)\(/,
|
||||
replace: "$&$self.Guilds="
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "APPLICATION_LIBRARY,render",
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: {
|
||||
match: /(\(0,\i\.jsx\))\(\i\..,{className:\i\(\)\.guilds,themeOverride:\i}\)/,
|
||||
replace: "$&,$1($self.FolderSideBar,{})"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: '("guildsnav")',
|
||||
predicate: () => settings.store.closeAllHomeButton,
|
||||
replacement: {
|
||||
match: ",onClick:function(){if(!__OVERLAY__){",
|
||||
replace: "$&$self.closeFolders();"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
const getGuildFolder = (id: string) => GuildFolderStore.getGuildFolders().find(f => f.guildIds.includes(id));
|
||||
|
||||
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onSwitch = data => {
|
||||
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
|
||||
return;
|
||||
|
||||
if (this.lastGuildId !== data.guildId) {
|
||||
this.lastGuildId = data.guildId;
|
||||
|
||||
const guildFolder = getGuildFolder(data.guildId);
|
||||
if (guildFolder?.folderId) {
|
||||
if (settings.store.forceOpen && !ExpandedFolderStore.isFolderExpanded(guildFolder.folderId))
|
||||
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
|
||||
} else if (settings.store.closeAllFolders)
|
||||
this.closeFolders();
|
||||
}
|
||||
});
|
||||
|
||||
FluxDispatcher.subscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder = e => {
|
||||
if (settings.store.closeOthers && !this.dispatching)
|
||||
FluxDispatcher.wait(() => {
|
||||
const expandedFolders = ExpandedFolderStore.getExpandedFolders();
|
||||
if (expandedFolders.size > 1) {
|
||||
this.dispatching = true;
|
||||
|
||||
for (const id of expandedFolders) if (id !== e.folderId)
|
||||
FolderUtils.toggleGuildFolderExpand(id);
|
||||
|
||||
this.dispatching = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
stop() {
|
||||
FluxDispatcher.unsubscribe("CHANNEL_SELECT", this.onSwitch);
|
||||
FluxDispatcher.unsubscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder);
|
||||
},
|
||||
|
||||
FolderSideBar,
|
||||
|
||||
getGuildsTree(folders, oldTree) {
|
||||
const tree = new GuildsTree();
|
||||
tree.root.children = oldTree.root.children.filter(e => folders.includes(e.id));
|
||||
tree.nodes = folders.map(id => oldTree.nodes[id]);
|
||||
return tree;
|
||||
},
|
||||
|
||||
closeFolders() {
|
||||
for (const id of ExpandedFolderStore.getExpandedFolders())
|
||||
FolderUtils.toggleGuildFolderExpand(id);
|
||||
},
|
||||
});
|
307
src/plugins/betterFolders/index.tsx
Normal file
307
src/plugins/betterFolders/index.tsx
Normal file
@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } 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 { FluxDispatcher, i18n } from "@webpack/common";
|
||||
|
||||
import FolderSideBar from "./FolderSideBar";
|
||||
|
||||
enum FolderIconDisplay {
|
||||
Never,
|
||||
Always,
|
||||
MoreThanOneFolderExpanded
|
||||
}
|
||||
|
||||
const GuildsTree = proxyLazy(() => findByProps("GuildsTree").GuildsTree);
|
||||
const SortedGuildStore = findStoreLazy("SortedGuildStore");
|
||||
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
|
||||
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
|
||||
|
||||
let lastGuildId = null as string | null;
|
||||
let dispatchingFoldersClose = false;
|
||||
|
||||
function getGuildFolder(id: string) {
|
||||
return SortedGuildStore.getGuildFolders().find(folder => folder.guildIds.includes(id));
|
||||
}
|
||||
|
||||
function closeFolders() {
|
||||
for (const id of ExpandedGuildFolderStore.getExpandedFolders())
|
||||
FolderUtils.toggleGuildFolderExpand(id);
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
sidebar: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Display servers from folder on dedicated sidebar",
|
||||
restartNeeded: true,
|
||||
default: true
|
||||
},
|
||||
sidebarAnim: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Animate opening the folder sidebar",
|
||||
default: true
|
||||
},
|
||||
closeAllFolders: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Close all folders when selecting a server not in a folder",
|
||||
default: false
|
||||
},
|
||||
closeAllHomeButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Close all folders when clicking on the home button",
|
||||
restartNeeded: true,
|
||||
default: false
|
||||
},
|
||||
closeOthers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Close other folders when opening a folder",
|
||||
default: false
|
||||
},
|
||||
forceOpen: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Force a folder to open when switching to a server of that folder",
|
||||
default: false
|
||||
},
|
||||
keepIcons: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Keep showing guild icons in the primary guild bar folder when it's open in the BetterFolders sidebar",
|
||||
restartNeeded: true,
|
||||
default: false
|
||||
},
|
||||
showFolderIcon: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Show the folder icon above the folder guilds in the BetterFolders sidebar",
|
||||
options: [
|
||||
{ label: "Never", value: FolderIconDisplay.Never },
|
||||
{ label: "Always", value: FolderIconDisplay.Always, default: true },
|
||||
{ label: "When more than one folder is expanded", value: FolderIconDisplay.MoreThanOneFolderExpanded }
|
||||
],
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterFolders",
|
||||
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
|
||||
authors: [Devs.juby, Devs.AutumnVN, Devs.Nuckyz],
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: '("guildsnav")',
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: [
|
||||
// Create the isBetterFolders variable in the GuildsBar component
|
||||
{
|
||||
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/,
|
||||
replace: ",isBetterFolders"
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
||||
{
|
||||
match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
|
||||
replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},arguments[0].betterFoldersExpandedIds)`
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
||||
{
|
||||
match: /lastTargetNode:\i\[\i\.length-1\].+?Fragment.+?\]}\)\]/,
|
||||
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0].isBetterFolders))"
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
||||
{
|
||||
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
|
||||
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
||||
},
|
||||
// Export the isBetterFolders variable to the folders component
|
||||
{
|
||||
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,)/,
|
||||
replace: 'isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// This is the parent folder component
|
||||
find: ".MAX_GUILD_FOLDER_NAME_LENGTH,",
|
||||
predicate: () => settings.store.sidebar && settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||
replacement: [
|
||||
{
|
||||
// Modify the expanded state to instead return the list of expanded folders
|
||||
match: /(useStateFromStores\).{0,20}=>)(\i\.\i)\.isFolderExpanded\(\i\)/,
|
||||
replace: (_, rest, ExpandedGuildFolderStore) => `${rest}${ExpandedGuildFolderStore}.getExpandedFolders()`,
|
||||
},
|
||||
{
|
||||
// Modify the expanded prop to use the boolean if the above patch fails, or check if the folder is expanded from the list if it succeeds
|
||||
// Also export the list of expanded folders to the child folder component if the patch above succeeds, else export undefined
|
||||
match: /(?<=folderNode:(\i),expanded:)\i(?=,)/,
|
||||
replace: (isExpandedOrExpandedIds, folderNote) => ""
|
||||
+ `typeof ${isExpandedOrExpandedIds}==="boolean"?${isExpandedOrExpandedIds}:${isExpandedOrExpandedIds}.has(${folderNote}.id),`
|
||||
+ `betterFoldersExpandedIds:${isExpandedOrExpandedIds} instanceof Set?${isExpandedOrExpandedIds}:void 0`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: ".FOLDER_ITEM_GUILD_ICON_MARGIN);",
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: [
|
||||
// We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)
|
||||
|
||||
// If we are rendering the normal GuildsBar sidebar, we make Discord think the folder is always collapsed to show better icons (the mini guild icons) and avoid transitions
|
||||
{
|
||||
predicate: () => settings.store.keepIcons,
|
||||
match: /(?<=let{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;)(?=let)/,
|
||||
replace: (_, isExpanded) => `${isExpanded}=!!arguments[0].isBetterFolders&&${isExpanded};`
|
||||
},
|
||||
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
|
||||
{
|
||||
predicate: () => !settings.store.keepIcons,
|
||||
match: /(?<=\.Messages\.SERVER_FOLDER_PLACEHOLDER.+?useTransition\)\()/,
|
||||
replace: "!!arguments[0].isBetterFolders&&"
|
||||
},
|
||||
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
|
||||
{
|
||||
predicate: () => !settings.store.keepIcons,
|
||||
match: /expandedFolderBackground,.+?,(?=\i\(\(\i,\i,\i\)=>{let{key.{0,45}ul)(?<=selected:\i,expanded:(\i),.+?)/,
|
||||
replace: (m, isExpanded) => `${m}!arguments[0].isBetterFolders&&${isExpanded}?null:`
|
||||
},
|
||||
{
|
||||
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
|
||||
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||
match: /(?<=\.wrapper,children:\[)/,
|
||||
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0].isBetterFolders,arguments[0].betterFoldersExpandedIds)&&"
|
||||
},
|
||||
{
|
||||
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
|
||||
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||
match: /(?<=\.expandedFolderBackground.+?}\),)(?=\i,)/,
|
||||
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0].isBetterFolders,arguments[0].betterFoldersExpandedIds)?null:"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "APPLICATION_LIBRARY,render",
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: {
|
||||
// Render the Better Folders sidebar
|
||||
match: /(?<=({className:\i\.guilds,themeOverride:\i})\))/,
|
||||
replace: ",$self.FolderSideBar($1)"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".Messages.DISCODO_DISABLED",
|
||||
predicate: () => settings.store.closeAllHomeButton,
|
||||
replacement: {
|
||||
// Close all folders when clicking the home button
|
||||
match: /(?<=onClick:\(\)=>{)(?=.{0,200}"discodo")/,
|
||||
replace: "$self.closeFolders();"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
flux: {
|
||||
CHANNEL_SELECT(data) {
|
||||
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
|
||||
return;
|
||||
|
||||
if (lastGuildId !== data.guildId) {
|
||||
lastGuildId = data.guildId;
|
||||
const guildFolder = getGuildFolder(data.guildId);
|
||||
|
||||
if (guildFolder?.folderId) {
|
||||
if (settings.store.forceOpen && !ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) {
|
||||
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
|
||||
}
|
||||
} else if (settings.store.closeAllFolders) {
|
||||
closeFolders();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
TOGGLE_GUILD_FOLDER_EXPAND(data) {
|
||||
if (settings.store.closeOthers && !dispatchingFoldersClose) {
|
||||
dispatchingFoldersClose = true;
|
||||
|
||||
FluxDispatcher.wait(() => {
|
||||
const expandedFolders = ExpandedGuildFolderStore.getExpandedFolders();
|
||||
|
||||
if (expandedFolders.size > 1) {
|
||||
for (const id of expandedFolders) if (id !== data.folderId)
|
||||
FolderUtils.toggleGuildFolderExpand(id);
|
||||
}
|
||||
|
||||
dispatchingFoldersClose = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set<any>) {
|
||||
if (!isBetterFolders || expandedFolderIds == null) return oldTree;
|
||||
|
||||
const newTree = new GuildsTree();
|
||||
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
||||
newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
||||
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
||||
newTree.nodes = Object.fromEntries(
|
||||
Object.entries(oldTree.nodes)
|
||||
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
||||
);
|
||||
|
||||
return newTree;
|
||||
},
|
||||
|
||||
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
||||
return child => {
|
||||
if (isBetterFolders) {
|
||||
return child?.props?.["aria-label"] === i18n.Messages.SERVERS;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
},
|
||||
|
||||
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
||||
return child => {
|
||||
if (isBetterFolders) {
|
||||
return "onScroll" in child.props;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
},
|
||||
|
||||
shouldShowFolderIconAndBackground(isBetterFolders: boolean, expandedFolderIds?: Set<any>) {
|
||||
if (!isBetterFolders) return true;
|
||||
|
||||
switch (settings.store.showFolderIcon) {
|
||||
case FolderIconDisplay.Never:
|
||||
return false;
|
||||
case FolderIconDisplay.Always:
|
||||
return true;
|
||||
case FolderIconDisplay.MoreThanOneFolderExpanded:
|
||||
return (expandedFolderIds?.size ?? 0) > 1;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
FolderSideBar: guildsBarProps => <FolderSideBar {...guildsBarProps} />,
|
||||
|
||||
closeFolders
|
||||
});
|
@ -45,7 +45,8 @@ export default definePlugin({
|
||||
],
|
||||
|
||||
altify(props: any) {
|
||||
if (props.alt && props.alt !== "GIF") return props.alt;
|
||||
props.alt ??= "GIF";
|
||||
if (props.alt !== "GIF") return props.alt;
|
||||
|
||||
let url: string = props.original || props.src;
|
||||
try {
|
||||
|
@ -32,10 +32,16 @@ export default definePlugin({
|
||||
{
|
||||
find: "hideNote:",
|
||||
all: true,
|
||||
// Some modules match the find but the replacement is returned untouched
|
||||
noWarn: true,
|
||||
predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide,
|
||||
replacement: {
|
||||
match: /hideNote:.+?(?=[,}])/g,
|
||||
replace: "hideNote:true",
|
||||
match: /hideNote:.+?(?=([,}].*?\)))/g,
|
||||
replace: (m, rest) => {
|
||||
const destructuringMatch = rest.match(/}=.+/);
|
||||
if (destructuringMatch == null) return "hideNote:!0";
|
||||
return m;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -30,6 +30,7 @@ const ActivityClassName = findByPropsLazy("activity", "buttonColor");
|
||||
const Colors = findByPropsLazy("profileColors");
|
||||
|
||||
async function getApplicationAsset(key: string): Promise<string> {
|
||||
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
|
||||
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
|
||||
}
|
||||
|
||||
@ -395,7 +396,7 @@ export default definePlugin({
|
||||
return (
|
||||
<>
|
||||
<Forms.FormText>
|
||||
Go to <Link href="https://discord.com/developers/applications">Discord Deverloper Portal</Link> to create an application and
|
||||
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
||||
get the application ID.
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
|
@ -63,7 +63,7 @@ async function embedDidMount(this: Component<Props>) {
|
||||
embed.rawTitle = titles[0].title;
|
||||
}
|
||||
|
||||
if (thumbnails[0]?.votes >= 0) {
|
||||
if (thumbnails[0]?.votes >= 0 && thumbnails[0].timestamp) {
|
||||
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
||||
}
|
||||
|
@ -23,12 +23,12 @@ import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||
import { Promisable } from "type-fest";
|
||||
|
||||
const StickersStore = findStoreLazy("StickersStore");
|
||||
const uploadEmoji = findByCodeLazy('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS(");
|
||||
const EmojiManager = findByPropsLazy("fetchEmoji", "uploadEmoji", "deleteEmoji");
|
||||
|
||||
interface Sticker {
|
||||
t: "Sticker";
|
||||
@ -106,7 +106,7 @@ async function cloneEmoji(guildId: string, emoji: Emoji) {
|
||||
reader.readAsDataURL(data);
|
||||
});
|
||||
|
||||
return uploadEmoji({
|
||||
return EmojiManager.uploadEmoji({
|
||||
guildId,
|
||||
name: emoji.name.split("~")[0],
|
||||
image: dataUrl
|
||||
@ -155,10 +155,15 @@ async function doClone(guildId: string, data: Sticker | Emoji) {
|
||||
type: Toasts.Type.SUCCESS,
|
||||
id: Toasts.genId()
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
let message = "Something went wrong (check console!)";
|
||||
try {
|
||||
message = JSON.parse(e.text).message;
|
||||
} catch { }
|
||||
|
||||
new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e);
|
||||
Toasts.show({
|
||||
message: "Oopsie something went wrong :( Check console!!!",
|
||||
message: "Failed to clone: " + message,
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId()
|
||||
});
|
||||
|
@ -24,35 +24,33 @@ import { getCurrentGuild } from "@utils/discord";
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UserStore } from "@webpack/common";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
|
||||
import type { Message } from "discord-types/general";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
||||
const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings");
|
||||
const ReaderFactory = findByPropsLazy("readerFactory");
|
||||
const StickerStore = findStoreLazy("StickersStore") as {
|
||||
getPremiumPacks(): StickerPack[];
|
||||
getAllGuildStickers(): Map<string, Sticker[]>;
|
||||
getStickerById(id: string): Sticker | undefined;
|
||||
};
|
||||
|
||||
function searchProtoClass(localName: string, parentProtoClass: any) {
|
||||
if (!parentProtoClass) return;
|
||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
||||
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
|
||||
|
||||
const field = parentProtoClass.fields.find(field => field.localName === localName);
|
||||
function searchProtoClassField(localName: string, protoClass: any) {
|
||||
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
|
||||
if (!field) return;
|
||||
|
||||
const getter: any = Object.values(field).find(value => typeof value === "function");
|
||||
return getter?.();
|
||||
const fieldGetter = Object.values(field).find(value => typeof value === "function") as any;
|
||||
return fieldGetter?.();
|
||||
}
|
||||
|
||||
const AppearanceSettingsProto = proxyLazy(() => searchProtoClass("appearance", PreloadedUserSettingsProtoHandler.ProtoClass));
|
||||
const ClientThemeSettingsProto = proxyLazy(() => searchProtoClass("clientThemeSettings", AppearanceSettingsProto));
|
||||
const PreloadedUserSettingsActionCreators = proxyLazy(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);
|
||||
const AppearanceSettingsActionCreators = proxyLazy(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
|
||||
const ClientThemeSettingsActionsCreators = proxyLazy(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
|
||||
|
||||
const USE_EXTERNAL_EMOJIS = 1n << 18n;
|
||||
const USE_EXTERNAL_STICKERS = 1n << 37n;
|
||||
@ -176,39 +174,46 @@ export default definePlugin({
|
||||
predicate: () => settings.store.enableEmojiBypass,
|
||||
replacement: [
|
||||
{
|
||||
// Create a variable for the intention of listing the emoji
|
||||
match: /(?<=,intention:(\i).+?;)/,
|
||||
replace: (_, intention) => `var fakeNitroIntention=${intention};`
|
||||
replace: (_, intention) => `let fakeNitroIntention=${intention};`
|
||||
},
|
||||
{
|
||||
// Send the intention of listing the emoji to the nitro permission check functions
|
||||
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
|
||||
replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
|
||||
},
|
||||
{
|
||||
// Disallow the emoji if the intention doesn't allow it
|
||||
match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||
replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
|
||||
},
|
||||
{
|
||||
// Make the emoji always available if the intention allows it
|
||||
match: /if\(!\i\.available/,
|
||||
replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
|
||||
}
|
||||
]
|
||||
},
|
||||
// Allow emojis and animated emojis to be sent everywhere
|
||||
{
|
||||
find: "canUseAnimatedEmojis:function",
|
||||
predicate: () => settings.store.enableEmojiBypass,
|
||||
replacement: {
|
||||
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g,
|
||||
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))(?=})/g,
|
||||
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
||||
}
|
||||
},
|
||||
// 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;"
|
||||
},
|
||||
},
|
||||
// Make stickers always available
|
||||
{
|
||||
find: "\"SENDABLE\"",
|
||||
predicate: () => settings.store.enableStickerBypass,
|
||||
@ -217,6 +222,7 @@ export default definePlugin({
|
||||
replace: "true?"
|
||||
}
|
||||
},
|
||||
// Allow streaming with high quality
|
||||
{
|
||||
find: "canUseHighVideoUploadQuality:function",
|
||||
predicate: () => settings.store.enableStreamQualityBypass,
|
||||
@ -230,6 +236,7 @@ export default definePlugin({
|
||||
};
|
||||
})
|
||||
},
|
||||
// Remove boost requirements to stream with high quality
|
||||
{
|
||||
find: "STREAM_FPS_OPTION.format",
|
||||
predicate: () => settings.store.enableStreamQualityBypass,
|
||||
@ -238,6 +245,7 @@ export default definePlugin({
|
||||
replace: ""
|
||||
}
|
||||
},
|
||||
// Allow client themes to be changeable
|
||||
{
|
||||
find: "canUseClientThemes:function",
|
||||
replacement: {
|
||||
@ -249,19 +257,22 @@ export default definePlugin({
|
||||
find: '.displayName="UserSettingsProtoStore"',
|
||||
replacement: [
|
||||
{
|
||||
// Overwrite incoming connection settings proto with our local settings
|
||||
match: /CONNECTION_OPEN:function\((\i)\){/,
|
||||
replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
|
||||
},
|
||||
{
|
||||
match: /=(\i)\.local;/,
|
||||
replace: (m, props) => `${m}${props}.local||$self.handleProtoChange(${props}.settings.proto);`
|
||||
// Overwrite non local proto changes with our local settings
|
||||
match: /let{settings:/,
|
||||
replace: "arguments[0].local||$self.handleProtoChange(arguments[0].settings.proto);$&"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Call our function to handle changing the gradient theme when selecting a new one
|
||||
{
|
||||
find: "updateTheme:function",
|
||||
find: ",updateTheme(",
|
||||
replacement: {
|
||||
match: /(function \i\(\i\){var (\i)=\i\.backgroundGradientPresetId.+?)(\i\.\i\.updateAsync.+?theme=(.+?);.+?\),\i\))/,
|
||||
match: /(function \i\(\i\){let{backgroundGradientPresetId:(\i).+?)(\i\.\i\.updateAsync.+?theme=(.+?),.+?},\i\))/,
|
||||
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
|
||||
}
|
||||
},
|
||||
@ -269,11 +280,13 @@ export default definePlugin({
|
||||
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
|
||||
replacement: [
|
||||
{
|
||||
// Call our function to decide whether the emoji link should be kept or not
|
||||
predicate: () => settings.store.transformEmojis,
|
||||
match: /1!==(\i)\.length\|\|1!==\i\.length/,
|
||||
replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])`
|
||||
},
|
||||
{
|
||||
// Patch the rendered message content to add fake nitro emojis or remove sticker links
|
||||
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
|
||||
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
|
||||
replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`
|
||||
@ -281,36 +294,41 @@ export default definePlugin({
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "renderEmbeds=function",
|
||||
find: "renderEmbeds(",
|
||||
replacement: [
|
||||
{
|
||||
// Call our function to decide whether the embed should be ignored or not
|
||||
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
|
||||
match: /(renderEmbeds=function\((\i)\){)(.+?embeds\.map\(\(function\((\i)\){)/,
|
||||
match: /(renderEmbeds\((\i)\){)(.+?embeds\.map\((\i)=>{)/,
|
||||
replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;`
|
||||
},
|
||||
{
|
||||
// Patch the stickers array to add fake nitro stickers
|
||||
predicate: () => settings.store.transformStickers,
|
||||
match: /renderStickersAccessories=function\((\i)\){var (\i)=\(0,\i\.\i\)\(\i\),/,
|
||||
replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message}),`
|
||||
match: /(?<=renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;)/,
|
||||
replace: (_, message, stickers) => `${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
|
||||
},
|
||||
{
|
||||
// Filter attachments to remove fake nitro stickers or emojis
|
||||
predicate: () => settings.store.transformStickers,
|
||||
match: /renderAttachments=function\(\i\){var \i=this,(\i)=\i.attachments.+?;/,
|
||||
match: /renderAttachments\(\i\){let{attachments:(\i).+?;/,
|
||||
replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: ".STICKER_IN_MESSAGE_HOVER,",
|
||||
find: ".Messages.STICKER_POPOUT_UNJOINED_PRIVATE_GUILD_DESCRIPTION.format",
|
||||
predicate: () => settings.store.transformStickers,
|
||||
replacement: [
|
||||
{
|
||||
match: /var (\i)=\i\.renderableSticker,.{0,50}closePopout.+?channel:\i,closePopout:\i,/,
|
||||
replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},`
|
||||
// Export the renderable sticker to be used in the fake nitro sticker notice
|
||||
match: /let{renderableSticker:(\i).{0,250}isGuildSticker.+?channel:\i,/,
|
||||
replace: (m, renderableSticker) => `${m}fakeNitroRenderableSticker:${renderableSticker},`
|
||||
},
|
||||
{
|
||||
match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/,
|
||||
replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!${props}.renderableSticker?.fake)`
|
||||
// Add the fake nitro sticker notice
|
||||
match: /(let \i,{sticker:\i,channel:\i,closePopout:\i.+?}=(\i).+?;)(.+?description:)(\i)(?=,sticker:\i)/,
|
||||
replace: (_, rest, props, rest2, reactNode) => `${rest}let{fakeNitroRenderableSticker}=${props};${rest2}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!fakeNitroRenderableSticker?.fake)`
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -318,6 +336,7 @@ export default definePlugin({
|
||||
find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,",
|
||||
predicate: () => settings.store.transformEmojis,
|
||||
replacement: {
|
||||
// Export the emoji node to be used in the fake nitro emoji notice
|
||||
match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<={node:(\i),.+?)/,
|
||||
replace: (m, node) => `${m}fakeNitroNode:${node},`
|
||||
}
|
||||
@ -326,10 +345,12 @@ export default definePlugin({
|
||||
find: ".Messages.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION",
|
||||
predicate: () => settings.store.transformEmojis,
|
||||
replacement: {
|
||||
// Add the fake nitro emoji notice
|
||||
match: /(?<=isDiscoverable:\i,emojiComesFromCurrentGuild:\i,.+?}=(\i).+?;)(.+?return )(.{0,1000}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(?=},)/,
|
||||
replace: (_, props, rest, reactNode) => `var fakeNitroNode=${props}.fakeNitroNode;${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},fakeNitroNode?.fake)`
|
||||
replace: (_, props, rest, reactNode) => `let{fakeNitroNode}=${props};${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!fakeNitroNode?.fake)`
|
||||
}
|
||||
},
|
||||
// Allow using custom app icons
|
||||
{
|
||||
find: "canUsePremiumAppIcons:function",
|
||||
replacement: {
|
||||
@ -337,6 +358,7 @@ export default definePlugin({
|
||||
replace: "$&return true;"
|
||||
}
|
||||
},
|
||||
// Separate patch for allowing using custom app icons
|
||||
{
|
||||
find: "location:\"AppIconHome\"",
|
||||
replacement: {
|
||||
@ -359,26 +381,30 @@ export default definePlugin({
|
||||
},
|
||||
|
||||
handleProtoChange(proto: any, user: any) {
|
||||
if (proto == null || typeof proto === "string" || !UserSettingsProtoStore || (!proto.appearance && !AppearanceSettingsProto)) return;
|
||||
if (proto == null || typeof proto === "string" || !UserSettingsProtoStore || !PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators) return;
|
||||
|
||||
const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;
|
||||
|
||||
if (premiumType !== 2) {
|
||||
proto.appearance ??= AppearanceSettingsProto.create();
|
||||
proto.appearance ??= AppearanceSettingsActionCreators.create();
|
||||
|
||||
if (UserSettingsProtoStore.settings.appearance?.theme != null) {
|
||||
proto.appearance.theme = UserSettingsProtoStore.settings.appearance.theme;
|
||||
const appearanceSettingsDummy = AppearanceSettingsActionCreators.create({
|
||||
theme: UserSettingsProtoStore.settings.appearance.theme
|
||||
});
|
||||
|
||||
proto.appearance.theme = appearanceSettingsDummy.theme;
|
||||
}
|
||||
|
||||
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null && ClientThemeSettingsProto) {
|
||||
const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({
|
||||
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null) {
|
||||
const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({
|
||||
backgroundGradientPresetId: {
|
||||
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
|
||||
}
|
||||
});
|
||||
|
||||
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummyProto;
|
||||
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId;
|
||||
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummy;
|
||||
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -387,26 +413,26 @@ export default definePlugin({
|
||||
const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0;
|
||||
if (premiumType === 2 || backgroundGradientPresetId == null) return original();
|
||||
|
||||
if (!AppearanceSettingsProto || !ClientThemeSettingsProto || !ReaderFactory) return;
|
||||
if (!PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators || !ProtoUtils) return;
|
||||
|
||||
const currentAppearanceProto = PreloadedUserSettingsProtoHandler.getCurrentValue().appearance;
|
||||
const currentAppearanceSettings = PreloadedUserSettingsActionCreators.getCurrentValue().appearance;
|
||||
|
||||
const newAppearanceProto = currentAppearanceProto != null
|
||||
? AppearanceSettingsProto.fromBinary(AppearanceSettingsProto.toBinary(currentAppearanceProto), ReaderFactory)
|
||||
: AppearanceSettingsProto.create();
|
||||
const newAppearanceProto = currentAppearanceSettings != null
|
||||
? AppearanceSettingsActionCreators.fromBinary(AppearanceSettingsActionCreators.toBinary(currentAppearanceSettings), ProtoUtils.BINARY_READ_OPTIONS)
|
||||
: AppearanceSettingsActionCreators.create();
|
||||
|
||||
newAppearanceProto.theme = theme;
|
||||
|
||||
const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({
|
||||
const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({
|
||||
backgroundGradientPresetId: {
|
||||
value: backgroundGradientPresetId
|
||||
}
|
||||
});
|
||||
|
||||
newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummyProto;
|
||||
newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId;
|
||||
newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummy;
|
||||
newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
|
||||
|
||||
const proto = PreloadedUserSettingsProtoHandler.ProtoClass.create();
|
||||
const proto = PreloadedUserSettingsActionCreators.ProtoClass.create();
|
||||
proto.appearance = newAppearanceProto;
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
@ -729,7 +755,7 @@ export default definePlugin({
|
||||
gif.finish();
|
||||
|
||||
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
||||
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
||||
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
||||
},
|
||||
|
||||
start() {
|
||||
|
@ -60,7 +60,7 @@ interface Instance {
|
||||
}
|
||||
|
||||
|
||||
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "searchInput");
|
||||
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchBarFullRow");
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
searchOption: {
|
||||
@ -182,7 +182,7 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
|
||||
ref={ref}
|
||||
autoFocus={true}
|
||||
className={containerClasses.searchBar}
|
||||
size={SearchBarComponent.Sizes.SMALL}
|
||||
size={SearchBarComponent.Sizes.MEDIUM}
|
||||
onChange={onChange}
|
||||
onClear={() => {
|
||||
setQuery("");
|
||||
|
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);
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -33,8 +33,8 @@ export default definePlugin({
|
||||
patches: [{
|
||||
find: ".handleSelectGIF=",
|
||||
replacement: {
|
||||
match: /\.handleSelectGIF=\i=>\{/,
|
||||
replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);"
|
||||
match: /\.handleSelectGIF=(\i)=>\{/,
|
||||
replace: ".handleSelectGIF=$1=>{if (!this.props.className) return $self.handleSelect($1);"
|
||||
}
|
||||
}],
|
||||
|
||||
|
@ -186,6 +186,13 @@ export default definePlugin({
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: ".carouselModal",
|
||||
replacement: {
|
||||
match: /(?<=\.carouselModal.{0,100}onClick:)\i,/,
|
||||
replace: "()=>{},"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
settings,
|
||||
|
@ -15,25 +15,17 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.vc-imgzoom-nearest-neighbor > img {
|
||||
image-rendering: pixelated; /* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */
|
||||
.vc-imgzoom-nearest-neighbor>img {
|
||||
image-rendering: pixelated;
|
||||
|
||||
/* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */
|
||||
}
|
||||
|
||||
/* make the carousel take up less space so we can click the backdrop and exit out of it */
|
||||
[class|="carouselModal"] {
|
||||
height: fit-content;
|
||||
box-shadow: none;
|
||||
[class*="modalCarouselWrapper_"] {
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
[class*="modalCarouselWrapper"] {
|
||||
height: fit-content;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
[class|="wrapper"]:has(> #vc-imgzoom-magnify-modal) {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
[class*="carouselModal_"] {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
@ -4,28 +4,58 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
import style from "./styles.css?managed";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
inlineVideo: {
|
||||
description: "Play videos without carousel modal",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
mediaLayoutType: {
|
||||
description: "Choose media layout type",
|
||||
type: OptionType.SELECT,
|
||||
restartNeeded: true,
|
||||
options: [
|
||||
{ label: "STATIC, render loading image but image isn't resposive, no problem unless discord window width is too small", value: "STATIC", default: true },
|
||||
{ label: "RESPONSIVE, image is responsive but not render loading image, cause messages shift when loaded", value: "RESPONSIVE" },
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "NoMosaic",
|
||||
authors: [Devs.AutumnVN],
|
||||
description: "Removes Discord new image mosaic",
|
||||
tags: ["image", "mosaic", "media"],
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".oneByTwoLayoutThreeGrid",
|
||||
replacement: [{
|
||||
match: /mediaLayoutType:\i\.\i\.MOSAIC/,
|
||||
replace: 'mediaLayoutType:"RESPONSIVE"'
|
||||
replace: "mediaLayoutType:$self.mediaLayoutType()",
|
||||
},
|
||||
{
|
||||
match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/,
|
||||
replace: '"INVALID"',
|
||||
},]
|
||||
}]
|
||||
},
|
||||
{
|
||||
find: "renderAttachments(",
|
||||
predicate: () => settings.store.inlineVideo,
|
||||
replacement: {
|
||||
match: /url:(\i)\.url\}\);return /,
|
||||
replace: "$&$1.content_type?.startsWith('image/')&&"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "Messages.REMOVE_ATTACHMENT_TOOLTIP_TEXT",
|
||||
@ -33,10 +63,17 @@ export default definePlugin({
|
||||
match: /\i===\i\.\i\.MOSAIC/,
|
||||
replace: "true"
|
||||
}
|
||||
}],
|
||||
}
|
||||
],
|
||||
|
||||
mediaLayoutType() {
|
||||
return settings.store.mediaLayoutType;
|
||||
},
|
||||
|
||||
start() {
|
||||
enableStyle(style);
|
||||
},
|
||||
|
||||
stop() {
|
||||
disableStyle(style);
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export default definePlugin({
|
||||
// = isPremiumAtLeast(user.premiumType, TIER_2)
|
||||
match: /=(?=\i\.\i\.isPremiumAtLeast\(null==(\i))/,
|
||||
// = user.banner && isPremiumAtLeast(user.premiumType, TIER_2)
|
||||
replace: "=$1?.banner&&"
|
||||
replace: "=(arguments[0]?.bannerSrc||$1?.banner)&&"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -25,15 +25,15 @@ export default definePlugin({
|
||||
authors: [Devs.rushii],
|
||||
patches: [
|
||||
{
|
||||
find: "setSystemTrayApplications:function",
|
||||
find: ",setSystemTrayApplications",
|
||||
replacement: [
|
||||
{
|
||||
match: /setBadge:function.+?},/,
|
||||
replace: "setBadge:function(){},"
|
||||
match: /setBadge\(\i\).+?},/,
|
||||
replace: "setBadge(){},"
|
||||
},
|
||||
{
|
||||
match: /setSystemTrayIcon:function.+?},/,
|
||||
replace: "setSystemTrayIcon:function(){},"
|
||||
match: /setSystemTrayIcon\(\i\).+?},/,
|
||||
replace: "setSystemTrayIcon(){},"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -55,10 +55,8 @@ export default definePlugin({
|
||||
}],
|
||||
isPrivateChannelRead(message: MessageJSON) {
|
||||
const channelType = ChannelStore.getChannel(message.channel_id)?.type;
|
||||
if (channelType !== ChannelType.DM && channelType !== ChannelType.GROUP_DM) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(channelType !== ChannelType.DM && channelType !== ChannelType.GROUP_DM) ||
|
||||
(channelType === ChannelType.DM && settings.store.channelToAffect === "group_dm") ||
|
||||
(channelType === ChannelType.GROUP_DM && settings.store.channelToAffect === "user_dm") ||
|
||||
(settings.store.allowMentions && message.mentions.some(m => m.id === UserStore.getCurrentUser().id)) ||
|
||||
|
@ -18,12 +18,12 @@
|
||||
|
||||
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";
|
||||
|
||||
const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user)\/(.+)(?:\?.+?)?$/;
|
||||
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
|
||||
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
|
||||
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
|
||||
|
||||
@ -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",
|
||||
@ -55,8 +57,8 @@ export default definePlugin({
|
||||
{
|
||||
find: "trackAnnouncementMessageLinkClicked({",
|
||||
replacement: {
|
||||
match: /(?<=handleClick:function\(\)\{return (\i)\}.+?)async function \1\(.+?\)\{/,
|
||||
replace: "$& if(await $self.handleLink(...arguments)) return;"
|
||||
match: /(?<=handleClick:function\(\)\{return (\i)\}.+?)function \1\(.+?\)\{/,
|
||||
replace: "async $& if(await $self.handleLink(...arguments)) return;"
|
||||
}
|
||||
},
|
||||
// Make Spotify profile activity links open in app on web
|
||||
@ -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);
|
||||
}
|
@ -135,9 +135,9 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
||||
<Text variant="text-md/normal">
|
||||
{
|
||||
permission.type === PermissionType.Role
|
||||
? role?.name || "Unknown Role"
|
||||
? role?.name ?? "Unknown Role"
|
||||
: permission.type === PermissionType.User
|
||||
? (user && getUniqueUsername(user)) || "Unknown User"
|
||||
? (user && getUniqueUsername(user)) ?? "Unknown User"
|
||||
: (
|
||||
<Flex style={{ gap: "0.2em", justifyItems: "center" }}>
|
||||
@owner
|
||||
|
@ -20,7 +20,8 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, Co
|
||||
import { Devs } from "@utils/constants";
|
||||
import { makeLazy } from "@utils/lazy";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { UploadHandler, UserUtils } from "@webpack/common";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
@ -35,8 +36,6 @@ const getFrames = makeLazy(() => Promise.all(
|
||||
))
|
||||
);
|
||||
|
||||
const fetchUser = findByCodeLazy(".USER(");
|
||||
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
||||
const UploadStore = findByPropsLazy("getUploads");
|
||||
|
||||
function loadImage(source: File | string) {
|
||||
@ -70,7 +69,7 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
|
||||
return opt.value;
|
||||
case "user":
|
||||
try {
|
||||
const user = await fetchUser(opt.value);
|
||||
const user = await UserUtils.getUser(opt.value);
|
||||
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
|
||||
} catch (err) {
|
||||
console.error("[petpet] Failed to fetch user\n", err);
|
||||
@ -175,7 +174,7 @@ export default definePlugin({
|
||||
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
|
||||
// Immediately after the command finishes, Discord clears all input, including pending attachments.
|
||||
// Thus, setTimeout is needed to make this execute after Discord cleared the input
|
||||
setTimeout(() => promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
|
||||
setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
@ -28,8 +30,8 @@ export default definePlugin({
|
||||
{
|
||||
find: ".nonMediaAttachment]",
|
||||
replacement: {
|
||||
match: /\.nonMediaAttachment\].{0,10}children:\[\S{3}/,
|
||||
replace: "$&&&$self.renderPiPButton(),"
|
||||
match: /\.nonMediaAttachment\]:!(\i).{0,10}children:\[(\S)/,
|
||||
replace: "$&,$1&&$2&&$self.renderPiPButton(),"
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -40,6 +42,7 @@ export default definePlugin({
|
||||
{tooltipProps => (
|
||||
<div
|
||||
{...tooltipProps}
|
||||
className="vc-pip-button"
|
||||
role="button"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
@ -70,7 +73,7 @@ export default definePlugin({
|
||||
>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="var(--interactive-normal)"
|
||||
fill="currentColor"
|
||||
d="M21 3a1 1 0 0 1 1 1v7h-2V5H4v14h6v2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm0 10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h8zm-1 2h-6v4h6v-4z"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -137,5 +137,5 @@ export default definePlugin({
|
||||
},
|
||||
],
|
||||
|
||||
previewIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }),
|
||||
chatBarIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }),
|
||||
});
|
||||
|
@ -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)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ export default definePlugin({
|
||||
{
|
||||
find: ".Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
||||
replacement: {
|
||||
match: /isExpanded:\i&&.*?,/,
|
||||
replace: "isExpanded:true,"
|
||||
match: /isExpanded:\i&&(.+?),/,
|
||||
replace: "isExpanded:$1,"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -70,18 +70,27 @@ export default definePlugin({
|
||||
// RenderLevel defines if a channel is hidden, collapsed in category, visible, etc
|
||||
find: ".CannotShow=",
|
||||
replacement: [
|
||||
// Remove the special logic for channels we don't have access to
|
||||
{
|
||||
match: /if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{if\(this\.id===\i\).+?threadIds:\i}}/,
|
||||
replace: ""
|
||||
},
|
||||
// Do not check for unreads when selecting the render level if the channel is hidden
|
||||
{
|
||||
match: /(?=!1===\i.\i\.hasRelevantUnread\(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`
|
||||
},
|
||||
// Remove permission checking for getRenderLevel function
|
||||
{
|
||||
match: /(?<=getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/,
|
||||
replace: " "
|
||||
@ -186,13 +195,29 @@ export default definePlugin({
|
||||
]
|
||||
},
|
||||
{
|
||||
// Hide New unreads box for hidden channels
|
||||
// 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
|
||||
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
|
||||
}
|
||||
},
|
||||
{
|
||||
// Make the old version of unreads box not visible for hidden channels
|
||||
find: "renderBottomUnread(){",
|
||||
replacement: {
|
||||
match: /(?=&&\i\.\i\.hasRelevantUnread\((\i\.record)\))/,
|
||||
replace: "&&!$self.isHiddenChannel($1)"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Make the state of the old version of unreads box not include hidden channels
|
||||
find: ".useFlattenedChannelIdListWithThreads)",
|
||||
replacement: {
|
||||
match: /(?=&&\i\.\i\.hasRelevantUnread\((\i)\))/,
|
||||
replace: "&&!$self.isHiddenChannel($1)"
|
||||
}
|
||||
},
|
||||
// Only render the channel header and buttons that work when transitioning to a hidden channel
|
||||
{
|
||||
find: "Missing channel in Channel.renderHeaderToolbar",
|
||||
@ -275,7 +300,7 @@ export default definePlugin({
|
||||
match: /MANAGE_ROLES.{0,90}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?\]}\)))/,
|
||||
replace: (m, component, channel) => {
|
||||
// Export the channel for the users allowed component patch
|
||||
component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,channel:${channel}`);
|
||||
component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,shcChannel:${channel}`);
|
||||
// Always render the component for multiple allowed users
|
||||
component = component.replace(canonicalizeMatch(/1!==\i\.length/), "true");
|
||||
|
||||
@ -290,22 +315,22 @@ export default definePlugin({
|
||||
{
|
||||
// Create a variable for the channel prop
|
||||
match: /maxUsers:\i,users:\i.+?=(\i).+?;/,
|
||||
replace: (m, props) => `${m}var channel=${props}.channel;`
|
||||
replace: (m, props) => `${m}let{shcChannel}=${props};`
|
||||
},
|
||||
{
|
||||
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
|
||||
match: /\i>0(?=&&.{0,60}renderPopout)/,
|
||||
replace: m => `($self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)?true:${m})`
|
||||
replace: m => `($self.isHiddenChannel(shcChannel,true)?true:${m})`
|
||||
},
|
||||
{
|
||||
// Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
||||
match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
|
||||
replace: (_, amount) => `($self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)&&${amount}<=0?0:1)`
|
||||
replace: (_, amount) => `($self.isHiddenChannel(shcChannel,true)&&${amount}<=0?0:1)`
|
||||
},
|
||||
{
|
||||
// Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
||||
match: /(?<="\+",)(\i)\+1/,
|
||||
replace: (m, amount) => `$self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)&&${amount}<=0?"":${m}`
|
||||
replace: (m, amount) => `$self.isHiddenChannel(shcChannel,true)&&${amount}<=0?"":${m}`
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -389,6 +414,22 @@ export default definePlugin({
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Make the chat input bar channel list contain hidden channels
|
||||
find: ",queryStaticRouteChannels(",
|
||||
replacement: [
|
||||
{
|
||||
// Make the getChannels call to GuildChannelStore return hidden channels
|
||||
match: /(?<=queryChannels\(\i\){.+?getChannels\(\i)(?=\))/,
|
||||
replace: ",true"
|
||||
},
|
||||
{
|
||||
// Avoid filtering out hidden channels from the channel list
|
||||
match: /(?<=queryChannels\(\i\){.+?isGuildChannelType\)\((\i)\.type\))(?=&&!\i\.\i\.can\()/,
|
||||
replace: "&&!$self.isHiddenChannel($1)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "\"^/guild-stages/(\\\\d+)(?:/)?(\\\\d+)?\"",
|
||||
replacement: {
|
||||
@ -416,7 +457,7 @@ export default definePlugin({
|
||||
{
|
||||
// Filter hidden channels from GuildChannelStore.getChannels unless told otherwise
|
||||
match: /(?<=getChannels\(\i)(\){.+?)return (.+?)}/,
|
||||
replace: (_, rest, channels) => `,shouldIncludeHidden=false${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden);}`
|
||||
replace: (_, rest, channels) => `,shouldIncludeHidden${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden??false);}`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -47,7 +47,7 @@ export default definePlugin({
|
||||
authors: [Devs.Rini, Devs.TheKodeToad],
|
||||
patches: [
|
||||
{
|
||||
find: '"Message Username"',
|
||||
find: ".useCanSeeRemixBadge)",
|
||||
replacement: {
|
||||
match: /(?<=onContextMenu:\i,children:).*?\}/,
|
||||
replace: "$self.renderUsername(arguments[0])}"
|
||||
|
@ -86,6 +86,7 @@ export default definePlugin({
|
||||
},
|
||||
{
|
||||
find: "ChannelTextAreaButtons",
|
||||
predicate: () => settings.store.showIcon,
|
||||
replacement: {
|
||||
match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
|
||||
replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()",
|
||||
|
@ -2,7 +2,7 @@
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-bottom: 1px solid var(--background-modifier-accent);
|
||||
|
||||
--vc-spotify-green: #1db954; /* so cusotm themes can easily change it */
|
||||
--vc-spotify-green: #1db954; /* so custom themes can easily change it */
|
||||
}
|
||||
|
||||
.theme-light #vc-spotify-player {
|
||||
@ -167,7 +167,7 @@
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] {
|
||||
/* these importants are neccessary, it applies a width and height through inline styles */
|
||||
/* these importants are necessary, it applies a width and height through inline styles */
|
||||
height: 10px !important;
|
||||
width: 10px !important;
|
||||
background-color: var(--interactive-normal);
|
||||
|
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;
|
||||
}
|
||||
});
|
@ -112,7 +112,7 @@ export default definePlugin({
|
||||
{
|
||||
find: "getCooldownTextStyle",
|
||||
replacement: {
|
||||
match: /(?<=(\i)\.length\?\i.\i\.Messages.THREE_USERS_TYPING\.format\({\i:(\i),\i:(\i),\i:\i}\):)\i\.\i\.Messages\.SEVERAL_USERS_TYPING/,
|
||||
match: /(?<=(\i)\.length\?\i.\i\.Messages.THREE_USERS_TYPING\.format\({\i:(\i),(?:\i:)?(\i),\i:\i}\):)\i\.\i\.Messages\.SEVERAL_USERS_TYPING/,
|
||||
replace: (_, users, a, b) => `$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2 })`
|
||||
},
|
||||
predicate: () => settings.store.alternativeFormatting
|
||||
|
@ -1,12 +1,16 @@
|
||||
.vc-toolbox-btn,
|
||||
.vc-toolbox-btn svg {
|
||||
.vc-toolbox-btn>svg {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.vc-toolbox-btn svg {
|
||||
.vc-toolbox-btn>svg {
|
||||
color: var(--interactive-normal);
|
||||
}
|
||||
|
||||
:is(.vc-toolbox-btn:hover, .vc-toolbox-btn[class*="selected"]) svg {
|
||||
.vc-toolbox-btn[class*="selected"]>svg {
|
||||
color: var(--interactive-active);
|
||||
}
|
||||
|
||||
.vc-toolbox-btn:hover>svg {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
@ -89,10 +89,10 @@ function VencordPopout(onClose: () => void) {
|
||||
);
|
||||
}
|
||||
|
||||
function VencordPopoutIcon() {
|
||||
function VencordPopoutIcon(isShown: boolean) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width={24} height={24}>
|
||||
<path fill="currentColor" d="M53 10h7v1h-1v1h-1v1h-1v1h-1v1h-1v1h5v1h-7v-1h1v-1h1v-1h1v-1h1v-1h1v-1h-5m-43 1v32h2v2h2v2h2v2h2v2h2v2h2v2h2v2h2v2h8v-2h2V46h-2v2h-2v2h-4v-2h-2v-2h-2v-2h-2v-2h-2v-2h-2V12m24 0v27h-2v3h4v-6h2v-2h4V12m13 2h5v1h-1v1h-1v1h-1v1h3v1h-5v-1h1v-1h1v-1h1v-1h-3m8 5h1v5h1v-1h1v1h-1v1h1v-1h1v1h-1v3h-1v1h-2v1h-1v1h1v-1h2v-1h1v2h-1v1h-2v1h-1v-1h-1v1h-6v-1h-1v-1h-1v-2h1v1h2v1h3v1h1v-1h-1v-1h-3v-1h-4v-4h1v-2h1v-1h1v-1h1v2h1v1h1v-1h1v1h-1v1h2v-2h1v-2h1v-1h1m-13 4h2v1h-1v4h1v2h1v1h1v1h1v1h4v1h-6v-1h-6v-1h-1v-5h1v-1h1v-2h2m17 3h1v3h-1v1h-1v1h-1v2h-2v-2h2v-1h1v-1h1m1 0h1v3h-1v1h-2v-1h1v-1h1m-30 2v8h-8v32h8v8h32v-8h8v-8H70v8H54V44h16v8h16v-8h-8v-8h-1v1h-7v-1h-2v1h-8v-1" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27" width={24} height={24}>
|
||||
<path fill="currentColor" d={isShown ? "M9 0h1v1h1v2h1v2h3V3h1V1h1V0h1v2h1v2h1v7h-1v-1h-3V9h1V6h-1v4h-3v1h1v-1h2v1h3v1h-1v1h-3v2h1v1h1v1h1v3h-1v4h-2v-1h-1v-4h-1v4h-1v1h-2v-4H9v-3h1v-1h1v-1h1v-2H9v-1H8v-1h3V6h-1v3h1v1H8v1H7V4h1V2h1M5 19h2v1h1v1h1v3H4v-1h2v-1H4v-2h1m15-1h2v1h1v2h-2v1h2v1h-5v-3h1v-1h1m4 3h4v1h-4" : "M0 0h7v1H6v1H5v1H4v1H3v1H2v1h5v1H0V6h1V5h1V4h1V3h1V2h1V1H0m13 2h5v1h-1v1h-1v1h-1v1h3v1h-5V7h1V6h1V5h1V4h-3m8 5h1v5h1v-1h1v1h-1v1h1v-1h1v1h-1v3h-1v1h-2v1h-1v1h1v-1h2v-1h1v2h-1v1h-2v1h-1v-1h-1v1h-6v-1h-1v-1h-1v-2h1v1h2v1h3v1h1v-1h-1v-1h-3v-1h-4v-4h1v-2h1v-1h1v-1h1v2h1v1h1v-1h1v1h-1v1h2v-2h1v-2h1v-1h1M8 14h2v1H9v4h1v2h1v1h1v1h1v1h4v1h-6v-1H5v-1H4v-5h1v-1h1v-2h2m17 3h1v3h-1v1h-1v1h-1v2h-2v-2h2v-1h1v-1h1m1 0h1v3h-1v1h-2v-1h1v-1h1"} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@ -114,7 +114,7 @@ function VencordPopoutButton() {
|
||||
className="vc-toolbox-btn"
|
||||
onClick={() => setShow(v => !v)}
|
||||
tooltip={isShown ? null : "Vencord Toolbox"}
|
||||
icon={VencordPopoutIcon}
|
||||
icon={() => VencordPopoutIcon(isShown)}
|
||||
selected={isShown}
|
||||
/>
|
||||
)}
|
||||
|
@ -173,7 +173,7 @@ export default definePlugin({
|
||||
patches: [
|
||||
// Make pfps clickable
|
||||
{
|
||||
find: "onAddFriend:function",
|
||||
find: "User Profile Modal - Context Menu",
|
||||
replacement: {
|
||||
match: /\{src:(\i)(?=,avatarDecoration)/,
|
||||
replace: "{src:$1,onClick:()=>$self.openImage($1)"
|
||||
|
@ -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
|
||||
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import { LazyComponent, useTimer } from "@utils/react";
|
||||
import { findByCode } from "@webpack";
|
||||
import { find } from "@webpack";
|
||||
|
||||
import { cl } from "./utils";
|
||||
|
||||
@ -25,7 +25,7 @@ interface VoiceMessageProps {
|
||||
src: string;
|
||||
waveform: string;
|
||||
}
|
||||
const VoiceMessage = LazyComponent<VoiceMessageProps>(() => findByCode('["onVolumeChange","volume","onMute"]'));
|
||||
const VoiceMessage = LazyComponent<VoiceMessageProps>(() => find(m => m.type?.toString().includes("waveform:")));
|
||||
|
||||
export type VoicePreviewOptions = {
|
||||
src?: string;
|
||||
|
@ -25,7 +25,7 @@ import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModa
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin from "@utils/types";
|
||||
import { chooseFile } from "@utils/web";
|
||||
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, FluxDispatcher, Forms, lodash, Menu, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
|
||||
import { ComponentType } from "react";
|
||||
|
||||
@ -35,7 +35,7 @@ import { cl } from "./utils";
|
||||
import { VoicePreview } from "./VoicePreview";
|
||||
import { VoiceRecorderWeb } from "./WebRecorder";
|
||||
|
||||
const CloudUpload = findLazy(m => m.prototype?.uploadFileToCloud);
|
||||
const CloudUtils = findByPropsLazy("CloudUpload");
|
||||
const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage");
|
||||
const PendingReplyStore = findStoreLazy("PendingReplyStore");
|
||||
const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel");
|
||||
@ -76,7 +76,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
|
||||
const reply = PendingReplyStore.getPendingReply(channelId);
|
||||
if (reply) FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId });
|
||||
|
||||
const upload = new CloudUpload({
|
||||
const upload = new CloudUtils.CloudUpload({
|
||||
file: new File([blob], "voice-message.ogg", { type: "audio/ogg; codecs=opus" }),
|
||||
isClip: false,
|
||||
isThumbnail: false,
|
||||
|
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;
|
||||
}
|
||||
}
|
@ -147,7 +147,7 @@ export default definePlugin({
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'navId:"textarea-context"',
|
||||
find: ".SLASH_COMMAND_SUGGESTIONS_TOGGLED,{",
|
||||
predicate: () => settings.store.addBack,
|
||||
replacement: [
|
||||
{
|
||||
|
@ -18,19 +18,14 @@
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findLazy, mapMangledModuleLazy } from "@webpack";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { ComponentDispatch, FluxDispatcher, NavigationRouter, SelectedGuildStore, SettingsRouter } from "@webpack/common";
|
||||
|
||||
const GuildNavBinds = mapMangledModuleLazy("mod+alt+down", {
|
||||
CtrlTab: m => m.binds?.at(-1) === "ctrl+tab",
|
||||
CtrlShiftTab: m => m.binds?.at(-1) === "ctrl+shift+tab",
|
||||
});
|
||||
|
||||
const DigitBinds = findLazy(m => m.binds?.[0] === "mod+1");
|
||||
const KeyBinds = findByPropsLazy("JUMP_TO_GUILD", "SERVER_NEXT");
|
||||
|
||||
export default definePlugin({
|
||||
name: "WebKeybinds",
|
||||
description: "Re-adds keybinds missing in the web version of Discord: ctrl+t, ctrl+shift+t, ctrl+tab, ctrl+shift+tab, ctrl+1-9, ctrl+,",
|
||||
description: "Re-adds keybinds missing in the web version of Discord: ctrl+t, ctrl+shift+t, ctrl+tab, ctrl+shift+tab, ctrl+1-9, ctrl+,. Only works fully on Vesktop/ArmCord, not inside your browser",
|
||||
authors: [Devs.Ven],
|
||||
enabledByDefault: true,
|
||||
|
||||
@ -57,13 +52,13 @@ export default definePlugin({
|
||||
SettingsRouter.open("My Account");
|
||||
break;
|
||||
case "Tab":
|
||||
const handler = e.shiftKey ? GuildNavBinds.CtrlShiftTab : GuildNavBinds.CtrlTab;
|
||||
const handler = e.shiftKey ? KeyBinds.SERVER_PREV : KeyBinds.SERVER_NEXT;
|
||||
handler.action(e);
|
||||
break;
|
||||
default:
|
||||
if (e.key >= "1" && e.key <= "9") {
|
||||
e.preventDefault();
|
||||
DigitBinds.action(e, `mod+${e.key}`);
|
||||
KeyBinds.JUMP_TO_GUILD.action(e, `mod+${e.key}`);
|
||||
}
|
||||
break;
|
||||
}
|
@ -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",
|
||||
}
|
||||
|
@ -379,6 +379,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
|
||||
|
@ -269,7 +269,7 @@ export interface DefinedSettings<
|
||||
store: SettingsStore<Def> & PrivateSettings;
|
||||
/**
|
||||
* React hook for getting the settings for this plugin
|
||||
* @param filter optional filter to avoid rerenders for irrelavent settings
|
||||
* @param filter optional filter to avoid rerenders for irrelevent settings
|
||||
*/
|
||||
use<F extends Extract<keyof Def | keyof PrivateSettings, string>>(filter?: F[]): Pick<SettingsStore<Def> & PrivateSettings, F>;
|
||||
/** Definitions of each setting */
|
||||
@ -307,3 +307,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;
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ import { proxyLazy } from "@utils/lazy";
|
||||
import type * as Stores from "discord-types/stores";
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { filters, findByCode, findByProps, findByPropsLazy, mapMangledModuleLazy } from "../webpack";
|
||||
import { filters, findByProps, findByPropsLazy, mapMangledModuleLazy } from "../webpack";
|
||||
import { waitForStore } from "./internal";
|
||||
import * as t from "./types/stores";
|
||||
|
||||
@ -84,14 +84,7 @@ export const useStateFromStores: <T>(
|
||||
idk?: any,
|
||||
isEqual?: (old: T, newer: T) => boolean
|
||||
) => T
|
||||
// FIXME: hack to support old stable and new canary
|
||||
= proxyLazy(() => {
|
||||
try {
|
||||
return findByProps("useStateFromStores").useStateFromStores;
|
||||
} catch {
|
||||
return findByCode('("useStateFromStores")');
|
||||
}
|
||||
});
|
||||
= proxyLazy(() => findByProps("useStateFromStores").useStateFromStores);
|
||||
|
||||
waitForStore("DraftStore", s => DraftStore = s);
|
||||
waitForStore("UserStore", s => UserStore = s);
|
||||
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import type { User } from "discord-types/general";
|
||||
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";
|
||||
@ -94,6 +94,9 @@ export function showToast(message: string, type = ToastType.MESSAGE) {
|
||||
}
|
||||
|
||||
export const UserUtils = findByPropsLazy("getUser", "fetchCurrentUser") as { getUser: (id: string) => Promise<User>; };
|
||||
export const UploadHandler = findByPropsLazy("showUploadFileSizeExceededError", "promptToUpload") as {
|
||||
promptToUpload: (files: File[], channel: Channel, draftType: Number) => void;
|
||||
};
|
||||
|
||||
export const ApplicationAssetUtils = findByPropsLazy("fetchAssetIds", "getAssetImage") as {
|
||||
fetchAssetIds: (applicationId: string, e: string[]) => Promise<string[]>;
|
||||
@ -133,11 +136,4 @@ waitFor("parseTopic", m => Parser = m);
|
||||
export let SettingsRouter: any;
|
||||
waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m);
|
||||
|
||||
// FIXME: hack to support old stable and new canary
|
||||
export const PermissionsBits: t.PermissionsBits = proxyLazy(() => {
|
||||
try {
|
||||
return find(m => m.Permissions?.ADMINISTRATOR).Permissions;
|
||||
} catch {
|
||||
return find(m => typeof m.ADMINISTRATOR === "bigint");
|
||||
}
|
||||
});
|
||||
export const PermissionsBits: t.PermissionsBits = proxyLazy(() => find(m => typeof m.Permissions?.ADMINISTRATOR === "bigint").Permissions);
|
||||
|
@ -29,7 +29,7 @@ let webpackChunk: any[];
|
||||
const logger = new Logger("WebpackInterceptor", "#8caaee");
|
||||
|
||||
if (window[WEBPACK_CHUNK]) {
|
||||
logger.info(`Patching ${WEBPACK_CHUNK}.push (was already existant, likely from cache!)`);
|
||||
logger.info(`Patching ${WEBPACK_CHUNK}.push (was already existent, likely from cache!)`);
|
||||
_initWebpack(window[WEBPACK_CHUNK]);
|
||||
patchPush(window[WEBPACK_CHUNK]);
|
||||
} else {
|
||||
@ -53,174 +53,35 @@ if (window[WEBPACK_CHUNK]) {
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// wreq.m is the webpack module factory.
|
||||
// normally, this is populated via webpackGlobal.push, which we patch below.
|
||||
// However, Discord has their .m prepopulated.
|
||||
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
|
||||
Object.defineProperty(Function.prototype, "m", {
|
||||
set(v: any) {
|
||||
// When using react devtools or other extensions, we may also catch their webpack here.
|
||||
// This ensures we actually got the right one
|
||||
if (new Error().stack?.includes("discord.com")) {
|
||||
logger.info("Found webpack module factory");
|
||||
patchFactories(v);
|
||||
|
||||
delete (Function.prototype as any).m;
|
||||
}
|
||||
|
||||
Object.defineProperty(this, "m", {
|
||||
value: v,
|
||||
configurable: true,
|
||||
});
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
function patchPush(webpackGlobal: any) {
|
||||
function handlePush(chunk: any) {
|
||||
try {
|
||||
const modules = chunk[1];
|
||||
const { subscriptions, listeners } = Vencord.Webpack;
|
||||
const { patches } = Vencord.Plugins;
|
||||
|
||||
for (const id in modules) {
|
||||
let mod = modules[id];
|
||||
// Discords Webpack chunks for some ungodly reason contain random
|
||||
// newlines. Cyn recommended this workaround and it seems to work fine,
|
||||
// however this could potentially break code, so if anything goes weird,
|
||||
// this is probably why.
|
||||
// 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;
|
||||
}
|
||||
const originalMod = mod;
|
||||
const patchedBy = new Set();
|
||||
|
||||
const factory = modules[id] = function (module, exports, require) {
|
||||
try {
|
||||
mod(module, exports, require);
|
||||
} catch (err) {
|
||||
// Just rethrow discord errors
|
||||
if (mod === originalMod) throw err;
|
||||
|
||||
logger.error("Error in patched chunk", err);
|
||||
return void originalMod(module, exports, require);
|
||||
}
|
||||
|
||||
exports = module.exports;
|
||||
|
||||
if (!exports) return;
|
||||
|
||||
// There are (at the time of writing) 11 modules exporting the window
|
||||
// Make these non enumerable to improve webpack search performance
|
||||
if (exports === window) {
|
||||
Object.defineProperty(require.c, id, {
|
||||
value: require.c[id],
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
} as any as { toString: () => string, original: any, (...args: any[]): void; };
|
||||
|
||||
// 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];
|
||||
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
|
||||
if (patch.predicate && !patch.predicate()) continue;
|
||||
|
||||
if (code.includes(patch.find)) {
|
||||
patchedBy.add(patch.plugin);
|
||||
|
||||
// we change all patch.replacement to array in plugins/index
|
||||
for (const replacement of patch.replacement as PatchReplacement[]) {
|
||||
if (replacement.predicate && !replacement.predicate()) continue;
|
||||
const lastMod = mod;
|
||||
const lastCode = code;
|
||||
|
||||
canonicalizeReplacement(replacement, patch.plugin);
|
||||
|
||||
try {
|
||||
const newCode = executePatch(replacement.match, replacement.replace as string);
|
||||
if (newCode === code && !patch.noWarn) {
|
||||
(window.explosivePlugins ??= new Set<string>()).add(patch.plugin);
|
||||
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
|
||||
if (IS_DEV) {
|
||||
logger.debug("Function Source:\n", code);
|
||||
}
|
||||
} else {
|
||||
code = newCode;
|
||||
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
|
||||
|
||||
if (IS_DEV) {
|
||||
const changeSize = code.length - lastCode.length;
|
||||
const match = lastCode.match(replacement.match)!;
|
||||
|
||||
// Use 200 surrounding characters of context
|
||||
const start = Math.max(0, match.index! - 200);
|
||||
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
|
||||
// (changeSize may be negative)
|
||||
const endPatched = end + changeSize;
|
||||
|
||||
const context = lastCode.slice(start, end);
|
||||
const patchedContext = code.slice(start, endPatched);
|
||||
|
||||
// inline require to avoid including it in !IS_DEV builds
|
||||
const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
|
||||
let fmt = "%c %s ";
|
||||
const elements = [] as string[];
|
||||
for (const d of diff) {
|
||||
const color = d.removed
|
||||
? "red"
|
||||
: d.added
|
||||
? "lime"
|
||||
: "grey";
|
||||
fmt += "%c%s";
|
||||
elements.push("color:" + color, d.value);
|
||||
}
|
||||
|
||||
logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
|
||||
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
|
||||
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
|
||||
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
|
||||
}
|
||||
code = lastCode;
|
||||
mod = lastMod;
|
||||
patchedBy.delete(patch.plugin);
|
||||
}
|
||||
}
|
||||
|
||||
if (!patch.all) patches.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
patchFactories(chunk[1]);
|
||||
} catch (err) {
|
||||
logger.error("Error in handlePush", err);
|
||||
}
|
||||
@ -229,13 +90,183 @@ function patchPush(webpackGlobal: any) {
|
||||
}
|
||||
|
||||
handlePush.$$vencordOriginal = webpackGlobal.push;
|
||||
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
|
||||
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
|
||||
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches
|
||||
// being applied multiple times.
|
||||
// Thus, override bind to use the original push
|
||||
handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args);
|
||||
|
||||
Object.defineProperty(webpackGlobal, "push", {
|
||||
get: () => handlePush,
|
||||
set(v) {
|
||||
delete webpackGlobal.push;
|
||||
webpackGlobal.push = v;
|
||||
patchPush(webpackGlobal);
|
||||
handlePush.$$vencordOriginal = v;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
function patchFactories(factories: Record<string | number, (module: { exports: any; }, exports: any, require: any) => void>) {
|
||||
const { subscriptions, listeners } = Vencord.Webpack;
|
||||
const { patches } = Vencord.Plugins;
|
||||
|
||||
for (const id in factories) {
|
||||
let mod = factories[id];
|
||||
// Discords Webpack chunks for some ungodly reason contain random
|
||||
// newlines. Cyn recommended this workaround and it seems to work fine,
|
||||
// however this could potentially break code, so if anything goes weird,
|
||||
// this is probably why.
|
||||
// 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;
|
||||
}
|
||||
const originalMod = mod;
|
||||
const patchedBy = new Set();
|
||||
|
||||
const factory = factories[id] = function (module, exports, require) {
|
||||
try {
|
||||
mod(module, exports, require);
|
||||
} catch (err) {
|
||||
// Just rethrow discord errors
|
||||
if (mod === originalMod) throw err;
|
||||
|
||||
logger.error("Error in patched chunk", err);
|
||||
return void originalMod(module, exports, require);
|
||||
}
|
||||
|
||||
exports = module.exports;
|
||||
|
||||
if (!exports) return;
|
||||
|
||||
// There are (at the time of writing) 11 modules exporting the window
|
||||
// Make these non enumerable to improve webpack search performance
|
||||
if (exports === window) {
|
||||
Object.defineProperty(require.c, id, {
|
||||
value: require.c[id],
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
} as any as { toString: () => string, original: any, (...args: any[]): void; };
|
||||
|
||||
// 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];
|
||||
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
|
||||
if (patch.predicate && !patch.predicate()) continue;
|
||||
|
||||
if (code.includes(patch.find)) {
|
||||
patchedBy.add(patch.plugin);
|
||||
|
||||
// we change all patch.replacement to array in plugins/index
|
||||
for (const replacement of patch.replacement as PatchReplacement[]) {
|
||||
if (replacement.predicate && !replacement.predicate()) continue;
|
||||
const lastMod = mod;
|
||||
const lastCode = code;
|
||||
|
||||
canonicalizeReplacement(replacement, patch.plugin);
|
||||
|
||||
try {
|
||||
const newCode = executePatch(replacement.match, replacement.replace as string);
|
||||
if (newCode === code && !patch.noWarn) {
|
||||
(window.explosivePlugins ??= new Set<string>()).add(patch.plugin);
|
||||
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
|
||||
if (IS_DEV) {
|
||||
logger.debug("Function Source:\n", code);
|
||||
}
|
||||
} else {
|
||||
code = newCode;
|
||||
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
|
||||
|
||||
if (IS_DEV) {
|
||||
const changeSize = code.length - lastCode.length;
|
||||
const match = lastCode.match(replacement.match)!;
|
||||
|
||||
// Use 200 surrounding characters of context
|
||||
const start = Math.max(0, match.index! - 200);
|
||||
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
|
||||
// (changeSize may be negative)
|
||||
const endPatched = end + changeSize;
|
||||
|
||||
const context = lastCode.slice(start, end);
|
||||
const patchedContext = code.slice(start, endPatched);
|
||||
|
||||
// inline require to avoid including it in !IS_DEV builds
|
||||
const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
|
||||
let fmt = "%c %s ";
|
||||
const elements = [] as string[];
|
||||
for (const d of diff) {
|
||||
const color = d.removed
|
||||
? "red"
|
||||
: d.added
|
||||
? "lime"
|
||||
: "grey";
|
||||
fmt += "%c%s";
|
||||
elements.push("color:" + color, d.value);
|
||||
}
|
||||
|
||||
logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
|
||||
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
|
||||
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
|
||||
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
|
||||
}
|
||||
code = lastCode;
|
||||
mod = lastMod;
|
||||
patchedBy.delete(patch.plugin);
|
||||
}
|
||||
}
|
||||
|
||||
if (!patch.all) patches.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,10 +63,10 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
|
||||
if (cache !== void 0) throw "no.";
|
||||
|
||||
instance.push([[Symbol("Vencord")], {}, r => wreq = r]);
|
||||
instance.pop();
|
||||
if (!wreq) return false;
|
||||
|
||||
cache = wreq.c;
|
||||
instance.pop();
|
||||
|
||||
for (const id in cache) {
|
||||
const { exports } = cache[id];
|
||||
|
Reference in New Issue
Block a user