Compare commits

..

4 Commits

Author SHA1 Message Date
Lewis Crichton
5d1736d020
feat: rework consent 2023-10-28 18:07:19 +01:00
Lewis Crichton
3de02708a6
chore: add TODO comment as reminder 2023-10-28 13:23:50 +01:00
Lewis Crichton
49c331fcc9
feat: anonymous telemetry 2023-10-28 13:19:59 +01:00
Lewis Crichton
ba53acdca7
chore: dedupe platform consts 2023-10-28 12:46:48 +01:00
45 changed files with 262 additions and 524 deletions

@ -14,8 +14,7 @@ body:
DO NOT USE THIS FORM, unless
- you are a vencord contributor
- you were given explicit permission to use this form by a moderator in our support server
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
- you are filing a security related report
- type: input
id: discord

@ -28,14 +28,6 @@ Visit https://vencord.dev/download
https://discord.gg/D9uwnFnqmd
## Sponsors
| **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!** |
|:--:|
| [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated) |
| *generated using [github-sponsor-graph](https://github.com/Vendicated/github-sponsor-graph)* |
## Star History
<a href="https://star-history.com/#Vendicated/Vencord&Timeline">

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.6.3",
"version": "1.6.1",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -70,7 +70,7 @@
"typescript": "^5.0.4",
"zip-local": "^0.3.5"
},
"packageManager": "pnpm@8.10.2",
"packageManager": "pnpm@8.1.1",
"pnpm": {
"patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",

@ -18,10 +18,8 @@
*/
import esbuild from "esbuild";
import { readdir } from "fs/promises";
import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
const defines = {
IS_STANDALONE: isStandalone,
@ -45,59 +43,13 @@ const nodeCommonOpts = {
format: "cjs",
platform: "node",
target: ["esnext"],
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
external: ["electron", "original-fs", ...commonOpts.external],
define: defines,
};
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
const sourcemap = watch ? "inline" : "external";
/**
* @type {import("esbuild").Plugin}
*/
const globNativesPlugin = {
name: "glob-natives-plugin",
setup: build => {
const filter = /^~pluginNatives$/;
build.onResolve({ filter }, args => {
return {
namespace: "import-natives",
path: args.path
};
});
build.onLoad({ filter, namespace: "import-natives" }, async () => {
const pluginDirs = ["plugins", "userplugins"];
let code = "";
let natives = "\n";
let i = 0;
for (const dir of pluginDirs) {
const dirPath = join("src", dir);
if (!await existsAsync(dirPath)) continue;
const plugins = await readdir(dirPath);
for (const p of plugins) {
if (!await existsAsync(join(dirPath, p, "native.ts"))) continue;
const nameParts = p.split(".");
const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);
// pluginName.thing.desktop -> PluginName.thing
const cleanPluginName = p[0].toUpperCase() + namePartsWithoutTarget.join(".").slice(1);
const mod = `p${i}`;
code += `import * as ${mod} from "./${dir}/${p}/native";\n`;
natives += `${JSON.stringify(cleanPluginName)}:${mod},\n`;
i++;
}
}
code += `export default {${natives}};`;
return {
contents: code,
resolveDir: "./src"
};
});
}
};
await Promise.all([
// Discord Desktop main & renderer & preload
esbuild.build({
@ -110,11 +62,7 @@ await Promise.all([
...defines,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
},
plugins: [
...nodeCommonOpts.plugins,
globNativesPlugin
]
}
}),
esbuild.build({
...commonOpts,
@ -159,11 +107,7 @@ await Promise.all([
...defines,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
},
plugins: [
...nodeCommonOpts.plugins,
globNativesPlugin
]
}
}),
esbuild.build({
...commonOpts,

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

@ -34,6 +34,7 @@ import { patches, PMLogger, startAllPlugins } from "./plugins";
import { localStorage } from "./utils/localStorage";
import { relaunch } from "./utils/native";
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
import { sendTelemetry } from "./utils/telemetry";
import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
@ -83,6 +84,8 @@ async function init() {
syncSettings();
sendTelemetry();
if (!IS_WEB) {
try {
const isOutdated = await checkForUpdates();

@ -7,7 +7,6 @@
import { IpcEvents } from "@utils/IpcEvents";
import { IpcRes } from "@utils/types";
import { ipcRenderer } from "electron";
import { PluginIpcMappings } from "main/ipcPlugins";
import type { UserThemeHeader } from "main/themes";
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
@ -18,16 +17,6 @@ export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.sendSync(event, ...args) as T;
}
const PluginHelpers = {} as Record<string, Record<string, (...args: any[]) => Promise<any>>>;
const pluginIpcMap = sendSync<PluginIpcMappings>(IpcEvents.GET_PLUGIN_IPC_METHOD_MAP);
for (const [plugin, methods] of Object.entries(pluginIpcMap)) {
const map = PluginHelpers[plugin] = {};
for (const [methodName, method] of Object.entries(methods)) {
map[methodName] = (...args: any[]) => invoke(method as IpcEvents, ...args);
}
}
export default {
themes: {
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
@ -72,5 +61,12 @@ export default {
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),
}
}
};

@ -61,6 +61,8 @@ export interface Settings {
settingsSync: boolean;
settingsSyncVersion: number;
};
telemetry?: boolean; // tri-state, undefined = ask
}
const DefaultSettings: Settings = {
@ -91,7 +93,9 @@ const DefaultSettings: Settings = {
url: "https://api.vencord.dev/",
settingsSync: false,
settingsSyncVersion: 0
}
},
telemetry: undefined
};
try {

@ -21,6 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import { ErrorCard } from "@components/ErrorCard";
import { isMac, isWindows } from "@utils/constants";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native";
@ -46,9 +47,6 @@ function VencordSettings() {
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
const isWindows = navigator.platform.toLowerCase().startsWith("win");
const isMac = navigator.platform.toLowerCase().startsWith("mac");
const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>;
title: string;
@ -93,6 +91,11 @@ function VencordSettings() {
key: "macosTranslucency",
title: "Enable translucent window",
note: "Requires a full restart"
},
{
key: "telemetry",
title: "Enable Telemetry",
note: "We only gather anonymous telemetry data. All data deleted after 3 days if you opt out."
}
];

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

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

@ -21,30 +21,16 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "AlwaysAnimate",
description: "Animates anything that can be animated",
description: "Animates anything that can be animated, besides status emojis.",
authors: [Devs.FieryFlames],
patches: [
{
find: "canAnimate:",
find: ".canAnimate",
all: true,
// Some modules match the find but the replacement is returned untouched
noWarn: true,
replacement: {
match: /canAnimate:.+?(?=([,}].*?\)))/g,
replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) return "canAnimate:!0";
return m;
}
}
},
{
// Status emojis
find: ".Messages.GUILD_OWNER,",
replacement: {
match: /(?<=\.activityEmoji,.+?animate:)\i/,
replace: "!0"
match: /\.canAnimate\b/g,
replace: ".canAnimate || true"
}
}
]

@ -18,40 +18,30 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { LazyComponent } from "@utils/react";
import { find, findByPropsLazy, findStoreLazy } from "@webpack";
import { useStateFromStores } from "@webpack/common";
import type { CSSProperties } from "react";
import { find, findByPropsLazy } from "@webpack";
import { React, useStateFromStores } from "@webpack/common";
import { ExpandedGuildFolderStore, settings } from ".";
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const Animations = findByPropsLazy("a", "animated", "useTransition");
const GuildsBar = LazyComponent(() => find(m => m.type?.toString().includes('("guildsnav")')));
export default ErrorBoundary.wrap(guildsBarProps => {
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
const isFullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
const Sidebar = (
<GuildsBar
{...guildsBarProps}
isBetterFolders={true}
betterFoldersExpandedIds={expandedFolders}
/>
);
const visible = !!expandedFolders.size;
const guilds = document.querySelector(guildsBarProps.className.split(" ").map(c => `.${c}`).join(""));
// We need to display none if we are in fullscreen. Yes this seems horrible doing with css, but it's literally how Discord does it.
// Also display flex otherwise to fix scrolling
const barStyle = {
display: isFullscreen ? "none" : "flex",
} as CSSProperties;
if (!guilds || !settings.store.sidebarAnim) {
return visible
? <div style={barStyle}>{Sidebar}</div>
? <div style={{ display: "flex " }}>{Sidebar}</div>
: null;
}
@ -63,9 +53,9 @@ export default ErrorBoundary.wrap(guildsBarProps => {
leave={{ width: 0 }}
config={{ duration: 200 }}
>
{(animationStyle, show) =>
{(style, show) =>
show && (
<Animations.animated.div style={{ ...animationStyle, ...barStyle }}>
<Animations.animated.div style={{ ...style, display: "flex" }}>
{Sidebar}
</Animations.animated.div>
)

@ -18,21 +18,13 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { proxyLazy } from "@utils/lazy";
import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findByPropsLazy, findStoreLazy } from "@webpack";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, i18n } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
enum FolderIconDisplay {
Never,
Always,
MoreThanOneFolderExpanded
}
const GuildsTree = proxyLazy(() => findByProps("GuildsTree").GuildsTree);
const SortedGuildStore = findStoreLazy("SortedGuildStore");
const GuildFolderStore = findStoreLazy("SortedGuildStore");
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
@ -40,7 +32,7 @@ let lastGuildId = null as string | null;
let dispatchingFoldersClose = false;
function getGuildFolder(id: string) {
return SortedGuildStore.getGuildFolders().find(folder => folder.guildIds.includes(id));
return GuildFolderStore.getGuildFolders().find(folder => folder.guildIds.includes(id));
}
function closeFolders() {
@ -58,6 +50,7 @@ export const settings = definePluginSettings({
sidebarAnim: {
type: OptionType.BOOLEAN,
description: "Animate opening the folder sidebar",
restartNeeded: true,
default: true
},
closeAllFolders: {
@ -86,16 +79,6 @@ export const settings = definePluginSettings({
description: "Keep showing guild icons in the primary guild bar folder when it's open in the BetterFolders sidebar",
restartNeeded: true,
default: false
},
showFolderIcon: {
type: OptionType.SELECT,
description: "Show the folder icon above the folder guilds in the BetterFolders sidebar",
options: [
{ label: "Never", value: FolderIconDisplay.Never },
{ label: "Always", value: FolderIconDisplay.Always, default: true },
{ label: "When more than one folder is expanded", value: FolderIconDisplay.MoreThanOneFolderExpanded }
],
restartNeeded: true
}
});
@ -116,45 +99,25 @@ export default definePlugin({
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/,
replace: ",isBetterFolders"
},
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
{
match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},arguments[0].betterFoldersExpandedIds)`
},
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{
match: /lastTargetNode:\i\[\i\.length-1\].+?Fragment.+?\]}\)\]/,
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0].isBetterFolders))"
replace: '$&.filter($self.makeGuildsBarGuildListFilter(typeof isBetterFolders!=="undefined"?isBetterFolders:false))'
},
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
{
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
replace: '$&.filter($self.makeGuildsBarTreeFilter(typeof isBetterFolders!=="undefined"?isBetterFolders:false))'
},
// Export the isBetterFolders variable to the folders component
{
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,)/,
replace: 'isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
}
]
},
// Avoid rendering servers that are not in folders in the Better Folders sidebar
{
// This is the parent folder component
find: ".MAX_GUILD_FOLDER_NAME_LENGTH,",
predicate: () => settings.store.sidebar && settings.store.showFolderIcon !== FolderIconDisplay.Always,
replacement: [
{
// Modify the expanded state to instead return the list of expanded folders
match: /(useStateFromStores\).{0,20}=>)(\i\.\i)\.isFolderExpanded\(\i\)/,
replace: (_, rest, ExpandedGuildFolderStore) => `${rest}${ExpandedGuildFolderStore}.getExpandedFolders()`,
},
{
// Modify the expanded prop to use the boolean if the above patch fails, or check if the folder is expanded from the list if it succeeds
// Also export the list of expanded folders to the child folder component if the patch above succeeds, else export undefined
match: /(?<=folderNode:(\i),expanded:)\i(?=,)/,
replace: (isExpandedOrExpandedIds, folderNote) => ""
+ `typeof ${isExpandedOrExpandedIds}==="boolean"?${isExpandedOrExpandedIds}:${isExpandedOrExpandedIds}.has(${folderNote}.id),`
+ `betterFoldersExpandedIds:${isExpandedOrExpandedIds} instanceof Set?${isExpandedOrExpandedIds}:void 0`
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?GUILD:)/,
replace: 'if((typeof isBetterFolders!=="undefined"?isBetterFolders:false)&&$1.parentId==null)return null;'
}
]
},
@ -162,37 +125,33 @@ export default definePlugin({
find: ".FOLDER_ITEM_GUILD_ICON_MARGIN);",
predicate: () => settings.store.sidebar,
replacement: [
// We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)
// Create the isBetterFolders variable in the nested folders component (the parent exports all the props so we don't have to patch it)
{
match: /(?<=let{folderNode:\i,setNodeRef:\i,)/,
replace: "isBetterFolders,"
},
// If we are rendering the normal GuildsBar sidebar, we make Discord think the folder is always collapsed to show better icons (the mini guild icons) and avoid transitions
{
predicate: () => settings.store.keepIcons,
match: /(?<=let{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;)(?=let)/,
replace: (_, isExpanded) => `${isExpanded}=!!arguments[0].isBetterFolders&&${isExpanded};`
match: /(?<=let{folderNode:\i,setNodeRef:\i,.+?expanded:(\i).+?;)(?=let)/,
replace: '$1=(typeof isBetterFolders!=="undefined"?isBetterFolders:false)?$1:false;'
},
// If we are rendering the Better Folders sidebar, we filter out folders that are not expanded
{
match: /(?=return\(0,\i.\i\)\("div")(?<=selected:\i,expanded:(\i),.+?)/,
replace: (_, expanded) => `if((typeof isBetterFolders!=="undefined"?isBetterFolders:false)&&!${expanded})return null;`
},
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{
predicate: () => !settings.store.keepIcons,
match: /(?<=\.Messages\.SERVER_FOLDER_PLACEHOLDER.+?useTransition\)\()/,
replace: "!!arguments[0].isBetterFolders&&"
replace: '(typeof isBetterFolders!=="undefined"?isBetterFolders:false)&&'
},
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
{
predicate: () => !settings.store.keepIcons,
match: /expandedFolderBackground,.+?,(?=\i\(\(\i,\i,\i\)=>{let{key.{0,45}ul)(?<=selected:\i,expanded:(\i),.+?)/,
replace: (m, isExpanded) => `${m}!arguments[0].isBetterFolders&&${isExpanded}?null:`
},
{
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.wrapper,children:\[)/,
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0].isBetterFolders,arguments[0].betterFoldersExpandedIds)&&"
},
{
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.expandedFolderBackground.+?}\),)(?=\i,)/,
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0].isBetterFolders,arguments[0].betterFoldersExpandedIds)?null:"
replace: (m, expanded) => `${m}((typeof isBetterFolders!=="undefined"?isBetterFolders:false)||!${expanded})&&`
}
]
},
@ -253,21 +212,6 @@ export default definePlugin({
}
},
getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set<any>) {
if (!isBetterFolders || expandedFolderIds == null) return oldTree;
const newTree = new GuildsTree();
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
newTree.nodes = Object.fromEntries(
Object.entries(oldTree.nodes)
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
);
return newTree;
},
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
return child => {
if (isBetterFolders) {
@ -286,21 +230,6 @@ export default definePlugin({
};
},
shouldShowFolderIconAndBackground(isBetterFolders: boolean, expandedFolderIds?: Set<any>) {
if (!isBetterFolders) return true;
switch (settings.store.showFolderIcon) {
case FolderIconDisplay.Never:
return false;
case FolderIconDisplay.Always:
return true;
case FolderIconDisplay.MoreThanOneFolderExpanded:
return (expandedFolderIds?.size ?? 0) > 1;
default:
return true;
}
},
FolderSideBar: guildsBarProps => <FolderSideBar {...guildsBarProps} />,
closeFolders

@ -30,7 +30,6 @@ const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const Colors = findByPropsLazy("profileColors");
async function getApplicationAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
}

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

@ -155,15 +155,10 @@ async function doClone(guildId: string, data: Sticker | Emoji) {
type: Toasts.Type.SUCCESS,
id: Toasts.genId()
});
} catch (e: any) {
let message = "Something went wrong (check console!)";
try {
message = JSON.parse(e.text).message;
} catch { }
} catch (e) {
new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e);
Toasts.show({
message: "Failed to clone: " + message,
message: "Oopsie something went wrong :( Check console!!!",
type: Toasts.Type.FAILURE,
id: Toasts.genId()
});

@ -19,7 +19,7 @@
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants";
import { Devs, isMac } from "@utils/constants";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
@ -96,9 +96,8 @@ export default definePlugin({
],
settingsAboutComponent: () => {
const isMacOS = navigator.platform.includes("Mac");
const modKey = isMacOS ? "cmd" : "ctrl";
const altKey = isMacOS ? "opt" : "alt";
const modKey = isMac ? "cmd" : "ctrl";
const altKey = isMac ? "opt" : "alt";
return (
<React.Fragment>
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle>

@ -201,15 +201,15 @@ export default definePlugin({
predicate: () => settings.store.enableEmojiBypass,
replacement: {
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
{
find: "canUseCustomStickersEverywhere:function",
find: "canUseStickersEverywhere:function",
predicate: () => settings.store.enableStickerBypass,
replacement: {
match: /canUseCustomStickersEverywhere:function\(\i\){/,
match: /canUseStickersEverywhere:function\(\i\){/,
replace: "$&return true;"
},
},

@ -60,7 +60,7 @@ interface Instance {
}
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchBarFullRow");
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "searchInput");
export const settings = definePluginSettings({
searchOption: {
@ -182,7 +182,7 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
ref={ref}
autoFocus={true}
className={containerClasses.searchBar}
size={SearchBarComponent.Sizes.MEDIUM}
size={SearchBarComponent.Sizes.SMALL}
onChange={onChange}
onClear={() => {
setQuery("");

@ -1,27 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { getSettings } from "main/ipcMain";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
const settings = getSettings().plugins?.FixSpotifyEmbeds;
if (!settings?.enabled) return;
frame.executeJavaScript(`
const original = Audio.prototype.play;
Audio.prototype.play = function() {
this.volume = ${(settings.volume / 100) || 0.1};
return original.apply(this, arguments);
}
`);
}
});
});
});

@ -33,8 +33,8 @@ export default definePlugin({
patches: [{
find: ".handleSelectGIF=",
replacement: {
match: /\.handleSelectGIF=(\i)=>\{/,
replace: ".handleSelectGIF=$1=>{if (!this.props.className) return $self.handleSelect($1);"
match: /\.handleSelectGIF=\i=>\{/,
replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);"
}
}],

@ -186,13 +186,6 @@ export default definePlugin({
}
]
},
{
find: ".carouselModal",
replacement: {
match: /(?<=\.carouselModal.{0,100}onClick:)\i,/,
replace: "()=>{},"
}
}
],
settings,

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

@ -4,58 +4,28 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin from "@utils/types";
import style from "./styles.css?managed";
const settings = definePluginSettings({
inlineVideo: {
description: "Play videos without carousel modal",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
},
mediaLayoutType: {
description: "Choose media layout type",
type: OptionType.SELECT,
restartNeeded: true,
options: [
{ label: "STATIC, render loading image but image isn't resposive, no problem unless discord window width is too small", value: "STATIC", default: true },
{ label: "RESPONSIVE, image is responsive but not render loading image, cause messages shift when loaded", value: "RESPONSIVE" },
]
}
});
export default definePlugin({
name: "NoMosaic",
authors: [Devs.AutumnVN],
description: "Removes Discord new image mosaic",
tags: ["image", "mosaic", "media"],
settings,
patches: [
{
find: ".oneByTwoLayoutThreeGrid",
replacement: [{
match: /mediaLayoutType:\i\.\i\.MOSAIC/,
replace: "mediaLayoutType:$self.mediaLayoutType()",
replace: 'mediaLayoutType:"RESPONSIVE"'
},
{
match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/,
replace: '"INVALID"',
}]
},
{
find: "renderAttachments(",
predicate: () => settings.store.inlineVideo,
replacement: {
match: /url:(\i)\.url\}\);return /,
replace: "$&$1.content_type?.startsWith('image/')&&"
}
},]
},
{
find: "Messages.REMOVE_ATTACHMENT_TOOLTIP_TEXT",
@ -63,17 +33,10 @@ export default definePlugin({
match: /\i===\i\.\i\.MOSAIC/,
replace: "true"
}
}
],
mediaLayoutType() {
return settings.store.mediaLayoutType;
},
}],
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}

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

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

@ -1,31 +0,0 @@
/*
* 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);
}

@ -4,8 +4,6 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
@ -30,8 +28,8 @@ export default definePlugin({
{
find: ".nonMediaAttachment]",
replacement: {
match: /\.nonMediaAttachment\]:!(\i).{0,10}children:\[(\S)/,
replace: "$&,$1&&$2&&$self.renderPiPButton(),"
match: /\.nonMediaAttachment\].{0,10}children:\[(\S)/,
replace: "$&,$1&&$self.renderPiPButton(),"
},
},
],
@ -42,7 +40,6 @@ export default definePlugin({
{tooltipProps => (
<div
{...tooltipProps}
className="vc-pip-button"
role="button"
style={{
cursor: "pointer",
@ -73,7 +70,7 @@ export default definePlugin({
>
<svg width="24px" height="24px" viewBox="0 0 24 24">
<path
fill="currentColor"
fill="var(--interactive-normal)"
d="M21 3a1 1 0 0 1 1 1v7h-2V5H4v14h6v2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm0 10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h8zm-1 2h-6v4h6v-4z"
/>
</svg>

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

@ -17,7 +17,7 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Devs, isMac } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
@ -25,7 +25,6 @@ import { Message } from "discord-types/general";
const Kangaroo = findByPropsLazy("jumpToMessage");
const isMac = navigator.platform.includes("Mac"); // bruh
let replyIdx = -1;
let editIdx = -1;

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

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

@ -1,11 +0,0 @@
# 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.

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

@ -112,7 +112,7 @@ export default definePlugin({
{
find: "getCooldownTextStyle",
replacement: {
match: /(?<=(\i)\.length\?\i.\i\.Messages.THREE_USERS_TYPING\.format\({\i:(\i),(?:\i:)?(\i),\i:\i}\):)\i\.\i\.Messages\.SEVERAL_USERS_TYPING/,
match: /(?<=(\i)\.length\?\i.\i\.Messages.THREE_USERS_TYPING\.format\({\i:(\i),\i:(\i),\i:\i}\):)\i\.\i\.Messages\.SEVERAL_USERS_TYPING/,
replace: (_, users, a, b) => `$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2 })`
},
predicate: () => settings.store.alternativeFormatting

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

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

@ -1,24 +0,0 @@
/*
* 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;
}
}

@ -18,14 +18,19 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { findLazy, mapMangledModuleLazy } from "@webpack";
import { ComponentDispatch, FluxDispatcher, NavigationRouter, SelectedGuildStore, SettingsRouter } from "@webpack/common";
const KeyBinds = findByPropsLazy("JUMP_TO_GUILD", "SERVER_NEXT");
const GuildNavBinds = mapMangledModuleLazy("mod+alt+down", {
CtrlTab: m => m.binds?.at(-1) === "ctrl+tab",
CtrlShiftTab: m => m.binds?.at(-1) === "ctrl+shift+tab",
});
const DigitBinds = findLazy(m => m.binds?.[0] === "mod+1");
export default definePlugin({
name: "WebKeybinds",
description: "Re-adds keybinds missing in the web version of Discord: ctrl+t, ctrl+shift+t, ctrl+tab, ctrl+shift+tab, ctrl+1-9, ctrl+,. Only works fully on Vesktop/ArmCord, not inside your browser",
description: "Re-adds keybinds missing in the web version of Discord: ctrl+t, ctrl+shift+t, ctrl+tab, ctrl+shift+tab, ctrl+1-9, ctrl+,",
authors: [Devs.Ven],
enabledByDefault: true,
@ -52,13 +57,13 @@ export default definePlugin({
SettingsRouter.open("My Account");
break;
case "Tab":
const handler = e.shiftKey ? KeyBinds.SERVER_PREV : KeyBinds.SERVER_NEXT;
const handler = e.shiftKey ? GuildNavBinds.CtrlShiftTab : GuildNavBinds.CtrlTab;
handler.action(e);
break;
default:
if (e.key >= "1" && e.key <= "9") {
e.preventDefault();
KeyBinds.JUMP_TO_GUILD.action(e, `mod+${e.key}`);
DigitBinds.action(e, `mod+${e.key}`);
}
break;
}

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

@ -379,10 +379,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ProffDea",
id: 609329952180928513n
},
ant0n: {
name: "ant0n",
id: 145224646868860928n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly
@ -393,3 +389,10 @@ export const DevsById = /* #__PURE__*/ (() =>
.map(([_, v]) => [v.id, v] as const)
))
)() as Record<string, Dev>;
const { platform } = navigator;
export const isWindows = platform.startsWith("Win");
export const isMac = platform.startsWith("Mac");
export const isLinux = platform.startsWith("Linux");

69
src/utils/telemetry.tsx Normal file

@ -0,0 +1,69 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Settings } from "@api/Settings";
import { Alerts } from "@webpack/common";
import { isPluginEnabled } from "../plugins";
import { Plugins } from "../Vencord";
import { isLinux, isMac, isWindows } from "./constants";
export function sendTelemetry() {
// TODO: READ THIS CHECK BEFORE RELEASING!!
// if (IS_DEV) return; // don't send on devbuilds, usually contains incorrect data
// if we have not yet told the user about the telemetry's existence, or they haven't agreed at all, DON'T send a
// probe now, but tell them and then let them decide if they want to opt in or not.
if (Settings.telemetry === undefined) {
Alerts.show({
title: "Telemetry Notice",
body: <>
<p>
Vencord has a telemetry feature that sends anonymous data to us, which we use to improve the mod. We
gather your operating system, the version of Vencord you're using and a list of enabled plugins, and
we can use this data to help improve it for yourself and everyone else.
</p>
<p>
If you don't want this, that's okay! We haven't sent anything yet. Please decide if you want to allow
us to gather a little bit of data. You can change this setting at any time in the future. If you
grant consent, we will start sending the data above the next time you reload or restart Discord.
</p>
</>,
confirmText: "Yes, that's fine",
cancelText: "No, I don't want that",
onConfirm() {
Settings.telemetry = true;
},
onCancel() {
Settings.telemetry = false;
}
});
return;
}
// if it's disabled in settings, obviously don't do anything
if (!Settings.telemetry) return;
const activePluginsList = Object.keys(Plugins.plugins)
.filter(p => isPluginEnabled(p));
let operatingSystem = "Unknown";
if (isWindows) operatingSystem = "Windows";
else if (isMac) operatingSystem = "macOS";
else if (isLinux) operatingSystem = "Linux";
const data = {
version: VERSION,
plugins: activePluginsList,
operatingSystem
};
navigator.sendBeacon("https://api.vencord.dev/v1/telemetry", JSON.stringify(data));
}

@ -307,10 +307,3 @@ export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon;
export type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = {
[key in keyof PluginExports]:
PluginExports[key] extends (event: Electron.IpcMainInvokeEvent, ...args: infer Args) => infer Return
? (...args: Args) => Return extends Promise<any> ? Return : Promise<Return>
: never;
};