Compare commits

..

32 Commits

Author SHA1 Message Date
V
ea11f2244f
README: Add sponsors 2023-11-13 01:37:15 +01:00
V
96126fa39f
remove pipebomb from github actions (#1968) 2023-11-09 07:15:49 +01:00
AM
9bd82943e3
[Plugin] Super Reaction Tweaks (#1958)
Co-authored-by: V <vendicated@riseup.net>
Co-authored-by: Jack Matthews <jm5112356@gmail.com>
2023-11-09 04:42:35 +01:00
V
5edc94062c
make packageManager key less specific 2023-11-09 02:42:34 +01:00
AutumnVN
394d2060eb
searchReply: fix (#1961) 2023-11-09 02:34:40 +01:00
V
119b628f33
feat: simple plugin natives (#1965) 2023-11-09 02:32:34 +01:00
zImPatrick
32f2043193
Fix FakeNitro sticker bypass (#1964) 2023-11-07 22:24:17 -03:00
Marvin Witt
04d2dd26c4
fix(dearrow): don't replace thumbnail if only original available (#1959) 2023-11-07 22:24:08 -03:00
Vendicated
86e94343cc
bump to v1.6.3 2023-11-05 02:07:33 +01:00
Vendicated
dd44ac1ad2
OpenInApp: Support podcasts 2023-11-04 19:08:44 +01:00
Vendicated
370b3d366d
OpenInApp: Fix links in messages 2023-11-04 18:59:16 +01:00
Vendicated
a67c7f841d
Fix ViewIcons correctly 2023-11-04 18:54:29 +01:00
Vendicated
2c9793202d
Revert "Fix ViewIcons"
This reverts commit 098da8c3fd95cdd1f299b86d0b919b001009a1df.
2023-11-04 18:54:02 +01:00
AutumnVN
a257926609
customRpc: fix discord attachment link (#1949) 2023-11-04 18:45:17 +01:00
AutumnVN
77659be4f0
fakeNitro: disallow emoji in add reaction (#1954) 2023-11-04 18:44:53 +01:00
AutumnVN
37b9a62460
favGifSearch: fix search bar (#1955) 2023-11-04 18:44:29 +01:00
Lewis Crichton
7f73e13364
docs: point people to advisories for security bugs (#1957) 2023-11-04 18:41:38 +01:00
Vendicated
fcf2bdda70
fix TypingTweaks 2023-11-03 02:03:58 +01:00
AutumnVN
fa9da2d693
noMosaic: play video inline + optional media layout type (#1946) 2023-11-03 01:57:39 +01:00
AutumnVN
44b21394b3
gifPaste: fix unable to use gif picker in profile customization (#1947) 2023-11-03 01:56:31 +01:00
Nuckyz
27fffc8bc3
Fix ShowMeYourName 2023-11-01 23:28:52 -03:00
Nuckyz
098da8c3fd
Fix ViewIcons 2023-11-01 23:26:13 -03:00
Nuckyz
9cf88d4232
Fix MessageDecorationsAPI 2023-11-01 22:46:06 -03:00
Vendicated
dd61b0c999
bump to v1.6.2 2023-11-01 02:25:13 +01:00
Haruka
5dc0d06be1
EmoteCloner: make the error toasts useful (#1938)
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-11-01 02:23:45 +01:00
AutumnVN
9de6c2d4ff
previewMessage: fix button (#1919) 2023-11-01 02:19:26 +01:00
AutumnVN
e37f62ac0a
imageZoom: dont close carousel modal on image click (#1926)
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-11-01 02:19:02 +01:00
AutumnVN
56a9d79f85
PiP: fix issues / styles (#1927) 2023-11-01 02:14:32 +01:00
Nuckyz
9d78233afa
Fix displaying BetterFolders sidebar when watching streams in fullscreen 2023-11-01 02:10:58 +01:00
Vendicated
9af2ec65ae
OnePingPerDM: fix server pings 2023-11-01 02:09:43 +01:00
Vendicated
584885acf5
[skip ci] Revert "add react linting"
doesnt work properly :(

This reverts commit 18fdc33ee7d1f60d58645c2a98f402988b97e996.
2023-10-31 23:56:13 +01:00
Vendicated
18fdc33ee7
[skip ci] add react linting 2023-10-31 23:50:55 +01:00
67 changed files with 491 additions and 1013 deletions

@ -9,7 +9,6 @@
"unused-imports", "unused-imports",
"path-alias" "path-alias"
], ],
"extends": ["plugin:react/recommended", "plugin:react/jsx-runtime"],
"settings": { "settings": {
"import/resolver": { "import/resolver": {
"alias": { "alias": {
@ -21,9 +20,6 @@
["@components", "./src/components"] ["@components", "./src/components"]
] ]
} }
},
"react": {
"version": "18.2"
} }
}, },
"rules": { "rules": {
@ -97,12 +93,6 @@
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"path-alias/no-relative": "error", "path-alias/no-relative": "error"
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
/* we dont target ancient browsers */
"react/jsx-no-target-blank": "off"
} }
} }

@ -14,7 +14,8 @@ body:
DO NOT USE THIS FORM, unless DO NOT USE THIS FORM, unless
- you are a vencord contributor - you are a vencord contributor
- you were given explicit permission to use this form by a moderator in our support server - 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 - type: input
id: discord id: discord

@ -28,6 +28,14 @@ Visit https://vencord.dev/download
https://discord.gg/D9uwnFnqmd 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 ## Star History
<a href="https://star-history.com/#Vendicated/Vencord&Timeline"> <a href="https://star-history.com/#Vendicated/Vencord&Timeline">

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.6.1", "version": "1.6.3",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -57,7 +57,6 @@
"eslint": "^8.46.0", "eslint": "^8.46.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "^1.0.0", "eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0", "highlight.js": "10.6.0",
@ -71,7 +70,7 @@
"typescript": "^5.0.4", "typescript": "^5.0.4",
"zip-local": "^0.3.5" "zip-local": "^0.3.5"
}, },
"packageManager": "pnpm@8.1.1", "packageManager": "pnpm@8.10.2",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch", "eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",

768
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

@ -18,8 +18,10 @@
*/ */
import esbuild from "esbuild"; 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 = { const defines = {
IS_STANDALONE: isStandalone, IS_STANDALONE: isStandalone,
@ -43,13 +45,59 @@ const nodeCommonOpts = {
format: "cjs", format: "cjs",
platform: "node", platform: "node",
target: ["esnext"], target: ["esnext"],
external: ["electron", "original-fs", ...commonOpts.external], external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
define: defines, define: defines,
}; };
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`; const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
const sourcemap = watch ? "inline" : "external"; 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([ await Promise.all([
// Discord Desktop main & renderer & preload // Discord Desktop main & renderer & preload
esbuild.build({ esbuild.build({
@ -62,7 +110,11 @@ await Promise.all([
...defines, ...defines,
IS_DISCORD_DESKTOP: true, IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false IS_VESKTOP: false
} },
plugins: [
...nodeCommonOpts.plugins,
globNativesPlugin
]
}), }),
esbuild.build({ esbuild.build({
...commonOpts, ...commonOpts,
@ -107,7 +159,11 @@ await Promise.all([
...defines, ...defines,
IS_DISCORD_DESKTOP: false, IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true IS_VESKTOP: true
} },
plugins: [
...nodeCommonOpts.plugins,
globNativesPlugin
]
}), }),
esbuild.build({ esbuild.build({
...commonOpts, ...commonOpts,

@ -20,8 +20,8 @@ import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js"; import "../checkNodeVersion.js";
import { exec, execSync } from "child_process"; import { exec, execSync } from "child_process";
import { existsSync, readFileSync } from "fs"; import { constants as FsConstants, readFileSync } from "fs";
import { readdir, readFile } from "fs/promises"; import { access, readdir, readFile } from "fs/promises";
import { join, relative } from "path"; import { join, relative } from "path";
import { promisify } from "util"; import { promisify } from "util";
@ -47,6 +47,12 @@ export const banner = {
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs")); 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 // https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/** /**
* @type {import("esbuild").Plugin} * @type {import("esbuild").Plugin}
@ -79,7 +85,7 @@ export const globPlugins = kind => ({
let plugins = "\n"; let plugins = "\n";
let i = 0; let i = 0;
for (const dir of pluginDirs) { for (const dir of pluginDirs) {
if (!existsSync(`./src/${dir}`)) continue; if (!await existsAsync(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`); const files = await readdir(`./src/${dir}`);
for (const file of files) { for (const file of files) {
if (file.startsWith("_") || file.startsWith(".")) continue; if (file.startsWith("_") || file.startsWith(".")) continue;

@ -7,6 +7,7 @@
import { IpcEvents } from "@utils/IpcEvents"; import { IpcEvents } from "@utils/IpcEvents";
import { IpcRes } from "@utils/types"; import { IpcRes } from "@utils/types";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { PluginIpcMappings } from "main/ipcPlugins";
import type { UserThemeHeader } from "main/themes"; import type { UserThemeHeader } from "main/themes";
function invoke<T = any>(event: IpcEvents, ...args: any[]) { 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; 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 { export default {
themes: { themes: {
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData), 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) openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
}, },
pluginHelpers: { pluginHelpers: 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),
}
}
}; };

@ -21,7 +21,7 @@ import "./styles.css";
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common"; import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
import { NotificationData } from "./Notifications"; import { NotificationData } from "./Notifications";

@ -80,7 +80,10 @@ const ErrorBoundary = LazyComponent(() => {
if (this.props.noop) return null; if (this.props.noop) return null;
if (this.props.fallback) if (this.props.fallback)
return <this.props.fallback {...this.state}>{this.props.children}</this.props.fallback>; return <this.props.fallback
children={this.props.children}
{...this.state}
/>;
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console."; const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";

@ -27,7 +27,7 @@ export function Heart() {
> >
<path <path
fill="#db61a2" fill="#db61a2"
fillRule="evenodd" fill-rule="evenodd"
d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"
/> />
</svg> </svg>

@ -58,7 +58,7 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
className={classes(className, "vc-link-icon")} className={classes(className, "vc-link-icon")}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<g fill="none" fillRule="evenodd"> <g fill="none" fill-rule="evenodd">
<path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z" /> <path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z" />
<rect width={width} height={height} /> <rect width={width} height={height} />
</g> </g>

@ -30,7 +30,7 @@ import { ChangeList } from "@utils/ChangeList";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { openModal } from "@utils/modal"; import { openModalLazy } from "@utils/modal";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
@ -95,14 +95,12 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
const isEnabled = () => settings.enabled ?? false; const isEnabled = () => settings.enabled ?? false;
function openPluginModal() { function openModal() {
openModal(modalProps => ( openModalLazy(async () => {
<PluginModal return modalProps => {
{...modalProps} return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
plugin={plugin} };
onRestartNeeded={() => onRestartNeeded(plugin.name)} });
/>
));
} }
function toggleEnabled() { function toggleEnabled() {
@ -161,7 +159,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
infoButton={ infoButton={
<button role="switch" onClick={() => openPluginModal()} className={classes(ButtonClasses.button, cl("info-button"))}> <button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
{plugin.options && !isObjectEmpty(plugin.options) {plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel /> ? <CogWheel />
: <InfoIcon />} : <InfoIcon />}
@ -354,7 +352,7 @@ function makeDependencyList(deps: string[]) {
return ( return (
<React.Fragment> <React.Fragment>
<Forms.FormText>This plugin is required by:</Forms.FormText> <Forms.FormText>This plugin is required by:</Forms.FormText>
{deps.map((dep: string) => <Forms.FormText key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)} {deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
</React.Fragment> </React.Fragment>
); );
} }

@ -106,9 +106,9 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
} }
function renderDiff() { function renderDiff() {
return diff?.map((p, i) => { return diff?.map(p => {
const color = p.added ? "lime" : p.removed ? "red" : "grey"; const color = p.added ? "lime" : p.removed ? "red" : "grey";
return <div key={i} style={{ color, userSelect: "text" }}>{p.value}</div>; return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
}); });
} }

@ -26,7 +26,7 @@ import { classes } from "@utils/misc";
import { showItemInFolder } from "@utils/native"; import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack"; import { findByPropsLazy, findLazy } from "@webpack";
import { Button, Card, FluxDispatcher, Forms, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import { UserThemeHeader } from "main/themes"; import { UserThemeHeader } from "main/themes";
import type { ComponentType, Ref, SyntheticEvent } from "react"; import type { ComponentType, Ref, SyntheticEvent } from "react";

@ -57,7 +57,7 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
title: "Oops!", title: "Oops!",
body: ( body: (
<ErrorCard> <ErrorCard>
{err.split("\n").map((line, i) => <div key={i}>{Parser.parse(line)}</div>)} {err.split("\n").map(line => <div>{Parser.parse(line)}</div>)}
</ErrorCard> </ErrorCard>
) )
}); });
@ -83,7 +83,7 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
return ( return (
<Card style={{ padding: ".5em" }}> <Card style={{ padding: ".5em" }}>
{updates.map(({ hash, author, message }) => ( {updates.map(({ hash, author, message }) => (
<div key={hash}> <div>
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code> <code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
<span style={{ <span style={{
marginLeft: "0.5em", marginLeft: "0.5em",

@ -17,73 +17,26 @@
*/ */
import { IpcEvents } from "@utils/IpcEvents"; import { IpcEvents } from "@utils/IpcEvents";
import { app, ipcMain } from "electron"; import { ipcMain } from "electron";
import { readFile } from "fs/promises";
import { request } from "https";
import { basename, normalize } from "path";
import { getSettings } from "./ipcMain"; import PluginNatives from "~pluginNatives";
// FixSpotifyEmbeds const PluginIpcMappings = {} as Record<string, Record<string, string>>;
app.on("browser-window-created", (_, win) => { export type PluginIpcMappings = typeof PluginIpcMappings;
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(` for (const [plugin, methods] of Object.entries(PluginNatives)) {
const original = Audio.prototype.play; const entries = Object.entries(methods);
Audio.prototype.play = function() { if (!entries.length) continue;
this.volume = ${(settings.volume / 100) || 0.1};
return original.apply(this, arguments);
}
`);
}
});
});
});
// #region OpenInApp const mappings = PluginIpcMappings[plugin] = {};
// These links don't support CORS, so this has to be native
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
function getRedirect(url: string) { for (const [methodName, method] of entries) {
return new Promise<string>((resolve, reject) => { const key = `VencordPluginNative_${plugin}_${methodName}`;
const req = request(new URL(url), { method: "HEAD" }, res => { ipcMain.handle(key, method);
resolve( mappings[methodName] = key;
res.headers.location }
? getRedirect(res.headers.location)
: url
);
});
req.on("error", reject);
req.end();
});
} }
ipcMain.handle(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, async (_, url: string) => { ipcMain.on(IpcEvents.GET_PLUGIN_IPC_METHOD_MAP, e => {
if (!validRedirectUrls.test(url)) return url; e.returnValue = PluginIpcMappings;
return getRedirect(url);
}); });
// #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

5
src/modules.d.ts vendored

@ -24,6 +24,11 @@ declare module "~plugins" {
export default 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" { declare module "~git-hash" {
const hash: string; const hash: string;
export default hash; export default hash;

@ -27,8 +27,8 @@ export default definePlugin({
{ {
find: '"Message Username"', find: '"Message Username"',
replacement: { replacement: {
match: /currentUserIsPremium:.{0,70}{children:\i(?=}\))/, match: /\.Messages\.GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE.+?}\),\i(?=\])/,
replace: "$&.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))" replace: "$&,...Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
} }
} }
], ],

@ -18,16 +18,19 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { LazyComponent } from "@utils/react"; import { LazyComponent } from "@utils/react";
import { find, findByPropsLazy } from "@webpack"; import { find, findByPropsLazy, findStoreLazy } from "@webpack";
import { useStateFromStores } from "@webpack/common"; import { useStateFromStores } from "@webpack/common";
import type { CSSProperties } from "react";
import { ExpandedGuildFolderStore, settings } from "."; import { ExpandedGuildFolderStore, settings } from ".";
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const Animations = findByPropsLazy("a", "animated", "useTransition"); const Animations = findByPropsLazy("a", "animated", "useTransition");
const GuildsBar = LazyComponent(() => find(m => m.type?.toString().includes('("guildsnav")'))); const GuildsBar = LazyComponent(() => find(m => m.type?.toString().includes('("guildsnav")')));
export default ErrorBoundary.wrap(guildsBarProps => { export default ErrorBoundary.wrap(guildsBarProps => {
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders()); const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
const isFullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
const Sidebar = ( const Sidebar = (
<GuildsBar <GuildsBar
@ -40,9 +43,15 @@ export default ErrorBoundary.wrap(guildsBarProps => {
const visible = !!expandedFolders.size; const visible = !!expandedFolders.size;
const guilds = document.querySelector(guildsBarProps.className.split(" ").map(c => `.${c}`).join("")); 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) { if (!guilds || !settings.store.sidebarAnim) {
return visible return visible
? <div style={{ display: "flex " }}>{Sidebar}</div> ? <div style={barStyle}>{Sidebar}</div>
: null; : null;
} }
@ -54,9 +63,9 @@ export default ErrorBoundary.wrap(guildsBarProps => {
leave={{ width: 0 }} leave={{ width: 0 }}
config={{ duration: 200 }} config={{ duration: 200 }}
> >
{(style, show) => {(animationStyle, show) =>
show && ( show && (
<Animations.animated.div style={{ ...style, display: "flex" }}> <Animations.animated.div style={{ ...animationStyle, ...barStyle }}>
{Sidebar} {Sidebar}
</Animations.animated.div> </Animations.animated.div>
) )

@ -21,6 +21,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { useTimer } from "@utils/react"; import { useTimer } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common";
function formatDuration(ms: number) { function formatDuration(ms: number) {
// here be dragons (moment fucking sucks) // here be dragons (moment fucking sucks)

@ -92,7 +92,6 @@ export default definePlugin({
fakeRenderWin = new WeakRef(win); fakeRenderWin = new WeakRef(win);
win.focus(); win.focus();
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(React.createElement(component, props), win.document.body); ReactDOM.render(React.createElement(component, props), win.document.body);
} }
}; };

@ -23,13 +23,14 @@ import { isTruthy } from "@utils/guards";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
const ActivityComponent = findByCodeLazy("onOpenGameProfile"); const ActivityComponent = findByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor"); const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const Colors = findByPropsLazy("profileColors"); const Colors = findByPropsLazy("profileColors");
async function getApplicationAsset(key: string): Promise<string> { 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]; return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
} }

@ -63,7 +63,7 @@ async function embedDidMount(this: Component<Props>) {
embed.rawTitle = titles[0].title; 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.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`; embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
} }

@ -155,10 +155,15 @@ async function doClone(guildId: string, data: Sticker | Emoji) {
type: Toasts.Type.SUCCESS, type: Toasts.Type.SUCCESS,
id: Toasts.genId() 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); new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e);
Toasts.show({ Toasts.show({
message: "Oopsie something went wrong :( Check console!!!", message: "Failed to clone: " + message,
type: Toasts.Type.FAILURE, type: Toasts.Type.FAILURE,
id: Toasts.genId() id: Toasts.genId()
}); });
@ -205,7 +210,7 @@ function CloneModal({ data }: { data: Sticker | Emoji; }) {
alignItems: "center" alignItems: "center"
}}> }}>
{guilds.map(g => ( {guilds.map(g => (
<Tooltip key={g.id} text={g.name}> <Tooltip text={g.name}>
{({ onMouseLeave, onMouseEnter }) => ( {({ onMouseLeave, onMouseEnter }) => (
<div <div
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
@ -278,27 +283,25 @@ function buildMenuItem(type: "Emoji" | "Sticker", fetchData: () => Promisable<Om
const data = { t: type, ...res } as Sticker | Emoji; const data = { t: type, ...res } as Sticker | Emoji;
const url = getUrl(data); const url = getUrl(data);
return function EmoteClonerModal(modalProps) { return modalProps => (
return ( <ModalRoot {...modalProps}>
<ModalRoot {...modalProps}> <ModalHeader>
<ModalHeader> <img
<img role="presentation"
role="presentation" aria-hidden
aria-hidden src={url}
src={url} alt=""
alt="" height={24}
height={24} width={24}
width={24} style={{ marginRight: "0.5em" }}
style={{ marginRight: "0.5em" }} />
/> <Forms.FormText>Clone {data.name}</Forms.FormText>
<Forms.FormText>Clone {data.name}</Forms.FormText> </ModalHeader>
</ModalHeader> <ModalContent>
<ModalContent> <CloneModal data={data} />
<CloneModal data={data} /> </ModalContent>
</ModalContent> </ModalRoot>
</ModalRoot> );
);
};
}) })
} }
/> />

@ -201,15 +201,15 @@ export default definePlugin({
predicate: () => settings.store.enableEmojiBypass, predicate: () => settings.store.enableEmojiBypass,
replacement: { 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)` replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
} }
}, },
// Allow stickers to be sent everywhere // Allow stickers to be sent everywhere
{ {
find: "canUseStickersEverywhere:function", find: "canUseCustomStickersEverywhere:function",
predicate: () => settings.store.enableStickerBypass, predicate: () => settings.store.enableStickerBypass,
replacement: { replacement: {
match: /canUseStickersEverywhere:function\(\i\){/, match: /canUseCustomStickersEverywhere:function\(\i\){/,
replace: "$&return true;" replace: "$&return true;"
}, },
}, },

@ -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({ export const settings = definePluginSettings({
searchOption: { searchOption: {
@ -182,7 +182,7 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
ref={ref} ref={ref}
autoFocus={true} autoFocus={true}
className={containerClasses.searchBar} className={containerClasses.searchBar}
size={SearchBarComponent.Sizes.SMALL} size={SearchBarComponent.Sizes.MEDIUM}
onChange={onChange} onChange={onChange}
onClear={() => { onClear={() => {
setQuery(""); setQuery("");

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

@ -28,7 +28,7 @@ import style from "./style.css?managed";
const Button = findByCodeLazy("Button.Sizes.NONE,disabled:"); const Button = findByCodeLazy("Button.Sizes.NONE,disabled:");
function makeIcon(showCurrentGame?: boolean) { function makeIcon(showCurrentGame?: boolean) {
return function GameActivityToggle() { return function () {
return ( return (
<svg <svg
width="20" width="20"

@ -33,8 +33,8 @@ export default definePlugin({
patches: [{ patches: [{
find: ".handleSelectGIF=", find: ".handleSelectGIF=",
replacement: { replacement: {
match: /\.handleSelectGIF=\i=>\{/, match: /\.handleSelectGIF=(\i)=>\{/,
replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);" 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, settings,

@ -15,19 +15,17 @@
border-radius: 0; border-radius: 0;
} }
.vc-imgzoom-nearest-neighbor > img { .vc-imgzoom-nearest-neighbor>img {
image-rendering: pixelated; /* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */ 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 */ /* make the carousel take up less space so we can click the backdrop and exit out of it */
[class|="carouselModal"] { [class*="modalCarouselWrapper_"] {
height: fit-content; top: 0 !important;
box-shadow: none;
} }
[class|="wrapper"]:has(> #vc-imgzoom-magnify-modal) { [class*="carouselModal_"] {
position: absolute; height: 0 !important;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
} }

@ -343,10 +343,10 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
? parse(message.content) ? parse(message.content)
: [noContent(message.attachments.length, message.embeds.length)] : [noContent(message.attachments.length, message.embeds.length)]
} }
{images.map((a, i) => { {images.map(a => {
const { width, height } = computeWidthAndHeight(a.width, a.height); const { width, height } = computeWidthAndHeight(a.width, a.height);
return ( return (
<div key={i}> <div>
<img src={a.url} width={width} height={height} /> <img src={a.url} width={width} height={height} />
</div> </div>
); );

@ -111,7 +111,7 @@ function SettingsComponent(props: { setValue(v: any): void; }) {
return ( return (
<Flex flexDirection="column"> <Flex flexDirection="column">
{tags.map(t => ( {tags.map(t => (
<Card key={t.name} style={{ padding: "1em 1em 0" }}> <Card style={{ padding: "1em 1em 0" }}>
<Forms.FormTitle style={{ width: "fit-content" }}> <Forms.FormTitle style={{ width: "fit-content" }}>
<Tooltip text={t.description}> <Tooltip text={t.description}>
{({ onMouseEnter, onMouseLeave }) => ( {({ onMouseEnter, onMouseLeave }) => (

@ -64,7 +64,6 @@ export default definePlugin({
renderMutualGDMs(user: User, onClose: () => void) { renderMutualGDMs(user: User, onClose: () => void) {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => ( const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
<Clickable <Clickable
key={c.id}
className={ProfileListClasses.listRow} className={ProfileListClasses.listRow}
onClick={() => { onClick={() => {
onClose(); onClose();

@ -4,28 +4,58 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles"; import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import style from "./styles.css?managed"; 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({ export default definePlugin({
name: "NoMosaic", name: "NoMosaic",
authors: [Devs.AutumnVN], authors: [Devs.AutumnVN],
description: "Removes Discord new image mosaic", description: "Removes Discord new image mosaic",
tags: ["image", "mosaic", "media"], tags: ["image", "mosaic", "media"],
settings,
patches: [ patches: [
{ {
find: ".oneByTwoLayoutThreeGrid", find: ".oneByTwoLayoutThreeGrid",
replacement: [{ replacement: [{
match: /mediaLayoutType:\i\.\i\.MOSAIC/, match: /mediaLayoutType:\i\.\i\.MOSAIC/,
replace: 'mediaLayoutType:"RESPONSIVE"' replace: "mediaLayoutType:$self.mediaLayoutType()",
}, },
{ {
match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/, match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/,
replace: '"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", find: "Messages.REMOVE_ATTACHMENT_TOOLTIP_TEXT",
@ -33,10 +63,17 @@ export default definePlugin({
match: /\i===\i\.\i\.MOSAIC/, match: /\i===\i\.\i\.MOSAIC/,
replace: "true" replace: "true"
} }
}], }
],
mediaLayoutType() {
return settings.store.mediaLayoutType;
},
start() { start() {
enableStyle(style); enableStyle(style);
}, },
stop() { stop() {
disableStyle(style); disableStyle(style);
} }

@ -55,10 +55,8 @@ export default definePlugin({
}], }],
isPrivateChannelRead(message: MessageJSON) { isPrivateChannelRead(message: MessageJSON) {
const channelType = ChannelStore.getChannel(message.channel_id)?.type; const channelType = ChannelStore.getChannel(message.channel_id)?.type;
if (channelType !== ChannelType.DM && channelType !== ChannelType.GROUP_DM) {
return false;
}
if ( if (
(channelType !== ChannelType.DM && channelType !== ChannelType.GROUP_DM) ||
(channelType === ChannelType.DM && settings.store.channelToAffect === "group_dm") || (channelType === ChannelType.DM && settings.store.channelToAffect === "group_dm") ||
(channelType === ChannelType.GROUP_DM && settings.store.channelToAffect === "user_dm") || (channelType === ChannelType.GROUP_DM && settings.store.channelToAffect === "user_dm") ||
(settings.store.allowMentions && message.mentions.some(m => m.id === UserStore.getCurrentUser().id)) || (settings.store.allowMentions && message.mentions.some(m => m.id === UserStore.getCurrentUser().id)) ||

@ -18,12 +18,12 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType, PluginNative } from "@utils/types";
import { showToast, Toasts } from "@webpack/common"; import { showToast, Toasts } from "@webpack/common";
import type { MouseEvent } from "react"; import type { MouseEvent } from "react";
const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/; 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 SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
const EpicMatcher = /^https:\/\/store\.epicgames\.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({ export default definePlugin({
name: "OpenInApp", name: "OpenInApp",
description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser", 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({", find: "trackAnnouncementMessageLinkClicked({",
replacement: { replacement: {
match: /(?<=handleClick:function\(\)\{return (\i)\}.+?)async function \1\(.+?\)\{/, match: /(?<=handleClick:function\(\)\{return (\i)\}.+?)function \1\(.+?\)\{/,
replace: "$& if(await $self.handleLink(...arguments)) return;" replace: "async $& if(await $self.handleLink(...arguments)) return;"
} }
}, },
// Make Spotify profile activity links open in app on web // Make Spotify profile activity links open in app on web
@ -84,7 +86,7 @@ export default definePlugin({
if (!IS_WEB && ShortUrlMatcher.test(url)) { if (!IS_WEB && ShortUrlMatcher.test(url)) {
event?.preventDefault(); event?.preventDefault();
// CORS jumpscare // CORS jumpscare
url = await VencordNative.pluginHelpers.OpenInApp.resolveRedirect(url); url = await Native.resolveRedirect(url);
} }
spotify: { spotify: {

@ -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);
}

@ -104,7 +104,6 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
return ( return (
<button <button
key={permission.id}
className={cl("perms-list-item-btn")} className={cl("perms-list-item-btn")}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
> >
@ -158,7 +157,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
</div> </div>
<div className={cl("perms-perms")}> <div className={cl("perms-perms")}>
{Object.entries(PermissionsBits).map(([permissionName, bit]) => ( {Object.entries(PermissionsBits).map(([permissionName, bit]) => (
<div key={permissionName} className={cl("perms-perms-item")}> <div className={cl("perms-perms-item")}>
<div className={cl("perms-perms-item-icon")}> <div className={cl("perms-perms-item-icon")}>
{(() => { {(() => {
const { permissions, overwriteAllow, overwriteDeny } = selectedItem; const { permissions, overwriteAllow, overwriteDeny } = selectedItem;

@ -109,7 +109,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
} }
defaultState={settings.store.defaultPermissionsDropdownState} defaultState={settings.store.defaultPermissionsDropdownState}
buttons={[ buttons={[
(<Tooltip key="sort" text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}> (<Tooltip text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
{tooltipProps => ( {tooltipProps => (
<button <button
{...tooltipProps} {...tooltipProps}
@ -133,7 +133,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
{userPermissions.length > 0 && ( {userPermissions.length > 0 && (
<div className={classes(root, roles)}> <div className={classes(root, roles)}>
{userPermissions.map(({ permission, roleColor }) => ( {userPermissions.map(({ permission, roleColor }) => (
<div key={permission} className={classes(role, rolePill, rolePillBorder)}> <div className={classes(role, rolePill, rolePillBorder)}>
<div className={roleRemoveButton}> <div className={roleRemoveButton}>
<span <span
className={roleCircle} className={roleCircle}

@ -4,11 +4,13 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import "./styles.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Tooltip } from "@webpack/common"; import { React, Tooltip } from "@webpack/common";
const settings = definePluginSettings({ const settings = definePluginSettings({
loop: { loop: {
@ -28,8 +30,8 @@ export default definePlugin({
{ {
find: ".nonMediaAttachment]", find: ".nonMediaAttachment]",
replacement: { replacement: {
match: /\.nonMediaAttachment\].{0,10}children:\[(\S)/, match: /\.nonMediaAttachment\]:!(\i).{0,10}children:\[(\S)/,
replace: "$&,$1&&$self.renderPiPButton()," replace: "$&,$1&&$2&&$self.renderPiPButton(),"
}, },
}, },
], ],
@ -40,6 +42,7 @@ export default definePlugin({
{tooltipProps => ( {tooltipProps => (
<div <div
{...tooltipProps} {...tooltipProps}
className="vc-pip-button"
role="button" role="button"
style={{ style={{
cursor: "pointer", cursor: "pointer",
@ -70,7 +73,7 @@ export default definePlugin({
> >
<svg width="24px" height="24px" viewBox="0 0 24 24"> <svg width="24px" height="24px" viewBox="0 0 24 24">
<path <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" 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> </svg>

@ -30,23 +30,21 @@ import { User } from "discord-types/general";
const SessionsStore = findStoreLazy("SessionsStore"); const SessionsStore = findStoreLazy("SessionsStore");
function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) { function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) {
return function PlatformIndicator({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) { return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => (
return ( <Tooltip text={tooltip} >
<Tooltip text={tooltip} > {(tooltipProps: any) => (
{(tooltipProps: any) => ( <svg
<svg {...tooltipProps}
{...tooltipProps} height={(opts?.height ?? 20) - (small ? 3 : 0)}
height={(opts?.height ?? 20) - (small ? 3 : 0)} width={(opts?.width ?? 20) - (small ? 3 : 0)}
width={(opts?.width ?? 20) - (small ? 3 : 0)} viewBox={opts?.viewBox ?? "0 0 24 24"}
viewBox={opts?.viewBox ?? "0 0 24 24"} fill={color}
fill={color} >
> <path d={path} />
<path d={path} /> </svg>
</svg> )}
)} </Tooltip>
</Tooltip> );
);
};
} }
const Icons = { const Icons = {

@ -137,5 +137,5 @@ export default definePlugin({
}, },
], ],
previewIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }), chatBarIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }),
}); });

@ -19,7 +19,7 @@
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, ReadStateStore } from "@webpack/common"; import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
function onClick() { function onClick() {
const channels: Array<any> = []; const channels: Array<any> = [];

@ -20,12 +20,12 @@ import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCal
import { ReplyIcon } from "@components/Icons"; import { ReplyIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { ChannelStore, i18n, Menu, PermissionsBits, PermissionStore, SelectedChannelStore } from "@webpack/common"; import { ChannelStore, i18n, Menu, PermissionsBits, PermissionStore, SelectedChannelStore } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
const replyFn = findByCodeLazy("showMentionToggle", "TEXTAREA_FOCUS", "shiftKey"); const messageUtils = findByPropsLazy("replyToMessage");
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => { const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => {
// make sure the message is in the selected channel // make sure the message is in the selected channel
@ -43,7 +43,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
id="reply" id="reply"
label={i18n.Messages.MESSAGE_ACTION_REPLY} label={i18n.Messages.MESSAGE_ACTION_REPLY}
icon={ReplyIcon} 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" id="reply"
label={i18n.Messages.MESSAGE_ACTION_REPLY} label={i18n.Messages.MESSAGE_ACTION_REPLY}
icon={ReplyIcon} icon={ReplyIcon}
action={(e: React.MouseEvent) => replyFn(channel, message, e)} action={(e: React.MouseEvent) => messageUtils.replyToMessage(channel, message, e)}
/> />
)); ));
} }

@ -172,7 +172,7 @@ export default definePlugin({
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<g fill="none" fillRule="evenodd"> <g fill="none" fill-rule="evenodd">
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" /> <path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
<rect width="24" height="24" /> <rect width="24" height="24" />
</g> </g>

@ -236,7 +236,6 @@ function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setC
<ScrollerThin fade className={cl("scroller")}> <ScrollerThin fade className={cl("scroller")}>
{members.map(id => {members.map(id =>
<FriendRow <FriendRow
key={id}
user={UserStore.getUser(id)} user={UserStore.getUser(id)}
status={PresenceStore.getStatus(id) || "offline"} status={PresenceStore.getStatus(id) || "offline"}
onSelect={() => openUserProfile(id)} onSelect={() => openUserProfile(id)}

@ -46,7 +46,7 @@ export const Code = ({
.split("\n") .split("\n")
.map((line, i) => <span key={i} dangerouslySetInnerHTML={{ __html: line }} />); .map((line, i) => <span key={i} dangerouslySetInnerHTML={{ __html: line }} />);
} catch { } catch {
lines = content.split("\n").map((line, i) => <span key={i}>{line}</span>); lines = content.split("\n").map(line => <span>{line}</span>);
} }
} else { } else {
const renderTokens = const renderTokens =
@ -55,11 +55,11 @@ export const Code = ({
.split("\n") .split("\n")
.map(line => [{ color: theme.plainColor, content: line } as IThemedToken]); .map(line => [{ color: theme.plainColor, content: line } as IThemedToken]);
lines = renderTokens.map((line, i) => { lines = renderTokens.map(line => {
// [Cynthia] this makes it so when you highlight the codeblock // [Cynthia] this makes it so when you highlight the codeblock
// empty lines are also selected and copied when you Ctrl+C. // empty lines are also selected and copied when you Ctrl+C.
if (line.length === 0) { if (line.length === 0) {
return <span key={i}>{"\n"}</span>; return <span>{"\n"}</span>;
} }
return ( return (

@ -18,7 +18,7 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { useAwaiter, useIntersection } from "@utils/react"; import { useAwaiter, useIntersection } from "@utils/react";
import { hljs } from "@webpack/common"; import { hljs, React } from "@webpack/common";
import { resolveLang } from "../api/languages"; import { resolveLang } from "../api/languages";
import { shiki } from "../api/shiki"; import { shiki } from "../api/shiki";

@ -105,7 +105,7 @@ function ConnectionsComponent({ id, theme }: { id: string, theme: string; }) {
gap: getSpacingPx(settings.store.iconSpacing), gap: getSpacingPx(settings.store.iconSpacing),
flexWrap: "wrap" flexWrap: "wrap"
}}> }}>
{connections.map(connection => <CompactConnectionComponent connection={connection} theme={theme} key={connection.id} />)} {connections.map(connection => <CompactConnectionComponent connection={connection} theme={theme} />)}
</Flex> </Flex>
</Section> </Section>
); );

@ -275,7 +275,7 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
<div className="shc-lock-screen-tags-container"> <div className="shc-lock-screen-tags-container">
<Text variant="text-lg/bold">Available tags:</Text> <Text variant="text-lg/bold">Available tags:</Text>
<div className="shc-lock-screen-tags"> <div className="shc-lock-screen-tags">
{availableTags.map(tag => <TagComponent tag={tag} key={tag.id} />)} {availableTags.map(tag => <TagComponent tag={tag} />)}
</div> </div>
</div> </div>
} }

@ -47,7 +47,7 @@ export default definePlugin({
authors: [Devs.Rini, Devs.TheKodeToad], authors: [Devs.Rini, Devs.TheKodeToad],
patches: [ patches: [
{ {
find: '"Message Username"', find: ".useCanSeeRemixBadge)",
replacement: { replacement: {
match: /(?<=onContextMenu:\i,children:).*?\}/, match: /(?<=onContextMenu:\i,children:).*?\}/,
replace: "$self.renderUsername(arguments[0])}" replace: "$self.renderUsername(arguments[0])}"

@ -88,7 +88,7 @@ function SilentMessageToggle(chatBoxProps: {
<g fill="currentColor"> <g fill="currentColor">
<path d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4C13 3.69264 13.0198 3.3899 13.0582 3.093C12.7147 3.03189 12.3611 3 12 3C8.686 3 6 5.686 6 9V14C6 15.657 4.656 17 3 17V18H21V17C19.344 17 18 15.657 18 14V10.7101ZM8.55493 19C9.24793 20.19 10.5239 21 11.9999 21C13.4759 21 14.7519 20.19 15.4449 19H8.55493Z" /> <path d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4C13 3.69264 13.0198 3.3899 13.0582 3.093C12.7147 3.03189 12.3611 3 12 3C8.686 3 6 5.686 6 9V14C6 15.657 4.656 17 3 17V18H21V17C19.344 17 18 15.657 18 14V10.7101ZM8.55493 19C9.24793 20.19 10.5239 21 11.9999 21C13.4759 21 14.7519 20.19 15.4449 19H8.55493Z" />
<path d="M18.2624 5.50209L21 2.5V1H16.0349V2.49791H18.476L16 5.61088V7H21V5.50209H18.2624Z" /> <path d="M18.2624 5.50209L21 2.5V1H16.0349V2.49791H18.476L16 5.61088V7H21V5.50209H18.2624Z" />
{!enabled && <line x1="22" y1="2" x2="2" y2="22" stroke="var(--red-500)" strokeWidth="2.5" />} {!enabled && <line x1="22" y1="2" x2="2" y2="22" stroke="var(--red-500)" stroke-width="2.5" />}
</g> </g>
</svg> </svg>
</div> </div>

@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, Tooltip } from "@webpack/common"; import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, React, Tooltip } from "@webpack/common";
const settings = definePluginSettings({ const settings = definePluginSettings({
showIcon: { showIcon: {
@ -62,7 +62,7 @@ function SilentTypingToggle(chatBoxProps: {
<div className={ButtonWrapperClasses.buttonWrapper}> <div className={ButtonWrapperClasses.buttonWrapper}>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"> <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path fill="currentColor" d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" /> <path fill="currentColor" d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" />
{isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" strokeWidth="72" strokeLinecap="round" />} {isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" stroke-width="72" stroke-linecap="round" />}
</svg> </svg>
</div> </div>
</Button> </Button>

@ -38,21 +38,19 @@ function msToHuman(ms: number) {
} }
function Svg(path: string, label: string) { function Svg(path: string, label: string) {
return function Icon() { return () => (
return ( <svg
<svg className={classes(cl("button-icon"), cl(label))}
className={classes(cl("button-icon"), cl(label))} height="24"
height="24" width="24"
width="24" viewBox="0 0 24 24"
viewBox="0 0 24 24" fill="currentColor"
fill="currentColor" aria-label={label}
aria-label={label} focusable={false}
focusable={false} >
> <path d={path} />
<path d={path} /> </svg>
</svg> );
);
};
} }
// KraXen's icons :yesyes: // KraXen's icons :yesyes:

@ -120,8 +120,8 @@ function ServerTrace({ trace }: ServerTraceProps) {
<Forms.FormSection title="Server Trace" tag="h2"> <Forms.FormSection title="Server Trace" tag="h2">
<code> <code>
<Flex flexDirection="column" style={{ color: "var(--header-primary)", gap: 5, userSelect: "text" }}> <Flex flexDirection="column" style={{ color: "var(--header-primary)", gap: 5, userSelect: "text" }}>
{lines.map((line, i) => ( {lines.map(line => (
<span key={i}>{line}</span> <span>{line}</span>
))} ))}
</Flex> </Flex>
</code> </code>

@ -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.

@ -0,0 +1,63 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated, ant0n, FieryFlames and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
export const settings = definePluginSettings({
superReactByDefault: {
type: OptionType.BOOLEAN,
description: "Reaction picker will default to Super Reactions",
default: true,
},
unlimitedSuperReactionPlaying: {
type: OptionType.BOOLEAN,
description: "Remove the limit on Super Reactions playing at once",
default: false,
},
superReactionPlayingLimit: {
description: "Max Super Reactions to play at once",
type: OptionType.SLIDER,
default: 20,
markers: [5, 10, 20, 40, 60, 80, 100],
stickToMarkers: true,
},
}, {
superReactionPlayingLimit: {
disabled() { return this.store.unlimitedSuperReactionPlaying; },
}
});
export default definePlugin({
name: "SuperReactionTweaks",
description: "Customize the limit of Super Reactions playing at once, and super react by default",
authors: [Devs.FieryFlames, Devs.ant0n],
patches: [
{
find: ",BURST_REACTION_EFFECT_PLAY",
replacement: {
match: /(?<=BURST_REACTION_EFFECT_PLAY:\i=>{.{50,100})(\i\(\i,\i\))>=\d+/,
replace: "!$self.shouldPlayBurstReaction($1)"
}
},
{
find: ".hasAvailableBurstCurrency)",
replacement: {
match: /(?<=\.useBurstReactionsExperiment.{0,20})useState\(!1\)(?=.+?(\i===\i\.EmojiIntention.REACTION))/,
replace: "useState($self.settings.store.superReactByDefault && $1)"
}
}
],
settings,
shouldPlayBurstReaction(playingCount: number) {
if (settings.store.unlimitedSuperReactionPlaying) return true;
if (playingCount <= settings.store.superReactionPlayingLimit) return true;
return false;
}
});

@ -21,7 +21,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { openUserProfile } from "@utils/discord"; import { openUserProfile } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Avatar, GuildMemberStore, RelationshipStore } from "@webpack/common"; import { Avatar, GuildMemberStore, React, RelationshipStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
const settings = definePluginSettings({ const settings = definePluginSettings({
@ -112,7 +112,7 @@ export default definePlugin({
{ {
find: "getCooldownTextStyle", find: "getCooldownTextStyle",
replacement: { 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 })` replace: (_, users, a, b) => `$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2 })`
}, },
predicate: () => settings.store.alternativeFormatting predicate: () => settings.store.alternativeFormatting
@ -135,7 +135,7 @@ export default definePlugin({
return children.map(c => return children.map(c =>
c.type === "strong" c.type === "strong"
? <TypingUser {...props} user={users[element++]} key={users[element].id} /> ? <TypingUser {...props} user={users[element++]} />
: c : c
); );
} }

@ -173,7 +173,7 @@ export default definePlugin({
patches: [ patches: [
// Make pfps clickable // Make pfps clickable
{ {
find: "onAddFriend:function", find: "User Profile Modal - Context Menu",
replacement: { replacement: {
match: /\{src:(\i)(?=,avatarDecoration)/, match: /\{src:(\i)(?=,avatarDecoration)/,
replace: "{src:$1,onClick:()=>$self.openImage($1)" replace: "{src:$1,onClick:()=>$self.openImage($1)"

@ -16,11 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { PluginNative } from "@utils/types";
import { Button, showToast, Toasts, useState } from "@webpack/common"; import { Button, showToast, Toasts, useState } from "@webpack/common";
import type { VoiceRecorder } from "."; import type { VoiceRecorder } from ".";
import { settings } from "./settings"; import { settings } from "./settings";
const Native = VencordNative.pluginHelpers.VoiceMessages as PluginNative<typeof import("./native")>;
export const VoiceRecorderDesktop: VoiceRecorder = ({ setAudioBlob, onRecordingChange }) => { export const VoiceRecorderDesktop: VoiceRecorder = ({ setAudioBlob, onRecordingChange }) => {
const [recording, setRecording] = useState(false); const [recording, setRecording] = useState(false);
@ -49,7 +52,7 @@ export const VoiceRecorderDesktop: VoiceRecorder = ({ setAudioBlob, onRecordingC
} else { } else {
discordVoice.stopLocalAudioRecording(async (filePath: string) => { discordVoice.stopLocalAudioRecording(async (filePath: string) => {
if (filePath) { if (filePath) {
const buf = await VencordNative.pluginHelpers.VoiceMessages.readRecording(filePath); const buf = await Native.readRecording(filePath);
if (buf) if (buf)
setAudioBlob(new Blob([buf], { type: "audio/ogg; codecs=opus" })); setAudioBlob(new Blob([buf], { type: "audio/ogg; codecs=opus" }));
else else

@ -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;
}
}

@ -38,6 +38,8 @@ export const enum IpcEvents {
BUILD = "VencordBuild", BUILD = "VencordBuild",
OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor", OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor",
GET_PLUGIN_IPC_METHOD_MAP = "VencordGetPluginIpcMethodMap",
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect", OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording", VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording",
} }

@ -379,6 +379,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ProffDea", name: "ProffDea",
id: 609329952180928513n id: 609329952180928513n
}, },
ant0n: {
name: "ant0n",
id: 145224646868860928n
},
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly

@ -133,7 +133,7 @@ export function useForceUpdater(withDep?: true) {
export function LazyComponent<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) { export function LazyComponent<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) {
const get = makeLazy(factory, attempts); const get = makeLazy(factory, attempts);
return function Lazy(props: T) { return (props: T) => {
const Component = get() ?? NoopComponent; const Component = get() ?? NoopComponent;
return <Component {...props} />; return <Component {...props} />;
}; };

@ -307,3 +307,10 @@ export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>; export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>; export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon; 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;
};