Compare commits

...

23 Commits
v1.6.2 ... main

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
29 changed files with 352 additions and 107 deletions

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.6.2", "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": {
@ -70,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",

View File

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

View File

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

View File

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

View File

@ -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
View File

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

View File

@ -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])"
} }
} }
], ],

View File

@ -30,6 +30,7 @@ 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];
} }

View File

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

View File

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

View File

@ -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("");

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

View File

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

View File

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

View File

@ -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: {

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

View File

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

View File

@ -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])}"

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

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

View File

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

View File

@ -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)"

View File

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

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

View File

@ -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",
} }

View File

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

View File

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