Compare commits

...

20 Commits

Author SHA1 Message Date
Vendicated
4aa7a052d0 Bump to v1.1.5 2023-04-04 21:29:39 +02:00
Vendicated
f088f17a0a Remove accidently introduced patch 2023-04-04 21:28:38 +02:00
Vendicated
a55c758b0e Fix SpotifyControls 2023-04-04 21:27:44 +02:00
Vendicated
f092f434fe Fix Vencord 2023-04-04 21:14:55 +02:00
Remty
2e6dfaa879 FakeProfileThemes: add usage guide (#778)
Co-authored-by: V <vendicated@riseup.net>
2023-04-04 13:28:41 +00:00
Nuckyz
96dc2e12d0 Fix Web & Game Activity Toggle (#777) 2023-04-04 15:26:53 +02:00
Đỗ Văn Hoài Tuân
d931790ed0 BetterFolders: Fix unread indicator & read all buttons being duplicated (#776) 2023-04-04 05:33:11 +02:00
V
6b26c12bfa Add additional build flavours for Vencord Desktop (#765) 2023-04-04 01:16:29 +02:00
Vendicated
5bb08bdb64 SpotifyControls: Fix crashing on canary
Vencord is still pretty broken on Canary and likely will be for a bit,
but this should at least fix instantly crashing
2023-04-03 21:25:14 +02:00
Vendicated
405be7ef13 Fix weird style on username sheet 2023-04-03 03:13:54 +02:00
Vendicated
a7e2fb48ba fix oopsie 2023-04-03 02:36:54 +02:00
Nuckyz
ae80749dd8 Game Activity Toggle and SettingsStoreAPI (#587) 2023-04-03 02:13:44 +02:00
Vendicated
8c47b7080d QuickReply & Up Key: Do not attempt to edit/reply to logged deleted message 2023-04-02 22:14:58 +02:00
Juby210
8378638ee4 BetterFolders: fix mentions display (#761)
closes #759
2023-04-02 20:31:10 +02:00
V
7c563471f6 Fix typo 2023-04-02 18:31:23 +02:00
Juby210
29382d2781 Add BetterFolders plugin (#530)
Co-authored-by: Ven <vendicated@riseup.net>
2023-04-02 17:43:06 +02:00
Vendicated
6226672ee8 Web: Update extension icon from trolley to Vencord logo 2023-04-02 16:55:36 +02:00
V
5b5ee82f27 Update Contributor Badge to new logo 2023-04-02 16:16:15 +02:00
Remty
62f74f5917 feat(plugin): FakeProfileThemes (#710) 2023-04-02 16:12:19 +02:00
V
265c7a18a7 Delete corruptMp4s.ts
Discord/Electron fixed this bug, so mp4s created by this plugin just look normal on Electron 22, not fixable
2023-04-02 04:33:17 +02:00
62 changed files with 1000 additions and 419 deletions

View File

@ -42,7 +42,7 @@ jobs:
- name: Clean up obsolete files
run: |
rm -rf dist/extension* Vencord.user.css
rm -rf dist/extension* Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
- name: Get some values needed for the release
id: release_values

View File

@ -50,7 +50,7 @@ jobs:
export CHROMIUM_BIN=$(which chromium-browser)
export USE_CANARY=true
esbuild test/generateReport.ts > dist/report.mjs
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}

View File

@ -6,7 +6,7 @@ The cutest Discord client mod
- Super easy to install (Download Installer, open, click install button, done)
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- Some highlights: SpotifyControls, GameActivityToggle, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- Fairly lightweight despite the many inbuilt plugins
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.1.4",
"version": "1.1.5",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -34,12 +34,12 @@
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"fflate": "^0.7.4",
"nanoid": "^4.0.2"
"nanoid": "^4.0.2",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@types/diff": "^5.0.2",
"@types/lodash": "^4.14.191",
"@types/nanoid": "^3.0.0",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",

16
pnpm-lock.yaml generated
View File

@ -11,7 +11,6 @@ patchedDependencies:
specifiers:
'@types/diff': ^5.0.2
'@types/lodash': ^4.14.191
'@types/nanoid': ^3.0.0
'@types/node': ^18.11.18
'@types/react': ^18.0.27
'@types/react-dom': ^18.0.10
@ -40,17 +39,18 @@ specifiers:
tsx: ^3.12.6
type-fest: ^3.5.3
typescript: ^4.9.4
virtual-merge: ^1.0.1
dependencies:
'@vap/core': 0.0.12
'@vap/shiki': 0.10.3
fflate: 0.7.4
nanoid: 4.0.2
virtual-merge: 1.0.1
devDependencies:
'@types/diff': 5.0.2
'@types/lodash': 4.14.191
'@types/nanoid': 3.0.0
'@types/node': 18.11.18
'@types/react': 18.0.27
'@types/react-dom': 18.0.10
@ -421,13 +421,6 @@ packages:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
dev: true
/@types/nanoid/3.0.0:
resolution: {integrity: sha512-UXitWSmXCwhDmAKe7D3hNQtQaHeHt5L8LO1CB8GF8jlYVzOv5cBWDNqiJ+oPEWrWei3i3dkZtHY/bUtd0R/uOQ==}
deprecated: This is a stub types definition. nanoid provides its own type definitions, so you do not need this installed.
dependencies:
nanoid: 4.0.2
dev: true
/@types/node/18.11.18:
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
dev: true
@ -2260,6 +2253,7 @@ packages:
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
engines: {node: ^14 || ^16 || >=18}
hasBin: true
dev: false
/nanomatch/1.2.13:
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
@ -3139,6 +3133,10 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
/virtual-merge/1.0.1:
resolution: {integrity: sha512-h7rzV6n5fZJbDu2lP4iu+IOtsZ00uqECFUxFePK1uY0pz/S5B7FNDJpmdDVfyGL7poyJECEHfTaIpJaknNkU0Q==}
dev: false
/vscode-oniguruma/1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
dev: false

View File

@ -48,6 +48,7 @@ const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.j
const sourcemap = watch ? "inline" : "external";
await Promise.all([
// common preload
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
@ -55,12 +56,19 @@ await Promise.all([
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
}),
// Discord Desktop main & renderer
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/patcher.ts"],
entryPoints: ["src/main/index.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VENCORD_DESKTOP: false
}
}),
esbuild.build({
...commonOpts,
@ -77,7 +85,43 @@ await Promise.all([
],
define: {
...defines,
IS_WEB: false
IS_WEB: false,
IS_DISCORD_DESKTOP: true,
IS_VENCORD_DESKTOP: false
}
}),
// Vencord Desktop main & renderer
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VENCORD_DESKTOP: true
}
}),
esbuild.build({
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/vencordDesktopRenderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins,
...commonOpts.plugins
],
define: {
...defines,
IS_WEB: false,
IS_DISCORD_DESKTOP: false,
IS_VENCORD_DESKTOP: true
}
}),
]).catch(err => {

View File

@ -45,7 +45,9 @@ const commonOptions = {
define: {
IS_WEB: "true",
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch)
IS_DEV: JSON.stringify(watch),
IS_DISCORD_DESKTOP: "false",
IS_VENCORD_DESKTOP: "false"
}
};

View File

@ -30,7 +30,7 @@ import "./webpack/patchWebpack";
import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins";
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
import { checkForUpdates, rebuild, update,UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
@ -56,8 +56,12 @@ async function init() {
permanent: true,
noPersist: true,
onClick() {
if (needsFullRestart)
if (needsFullRestart) {
if (IS_DISCORD_DESKTOP)
window.DiscordNative.app.relaunch();
else
window.VencordDesktop.app.relaunch();
}
else
location.reload();
}
@ -96,7 +100,7 @@ async function init() {
init();
if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
document.addEventListener("DOMContentLoaded", () => {
document.head.append(Object.assign(document.createElement("style"), {
id: "vencord-native-titlebar-style",

69
src/api/SettingsStore.ts Normal file
View File

@ -0,0 +1,69 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Logger from "@utils/Logger";
import { proxyLazy } from "@utils/proxyLazy";
import { findModuleId, wreq } from "@webpack";
import { Settings } from "./settings";
interface Setting<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T | ((old: T) => T)): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
*/
useSetting(): T;
settingsStoreApiGroup: string;
settingsStoreApiName: string;
}
const SettingsStores: Array<Setting<any>> | undefined = proxyLazy(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"');
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
});
/**
* Get the store for a setting
* @param group The setting group
* @param name The name of the setting
*/
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
}
/**
* getSettingStore but lazy
*/
export function getSettingStoreLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getSettingStore<T>(group, name));
}

View File

@ -28,6 +28,7 @@ import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $SettingsStore from "./SettingsStore";
import * as $Styles from "./Styles";
/**
@ -85,6 +86,10 @@ export const MessageDecorations = $MessageDecorations;
* An API allowing you to add components to member list users, in both DM's and servers
*/
export const MemberListDecorators = $MemberListDecorators;
/**
* An API allowing you to read, manipulate and automatically update components based on Discord settings
*/
export const SettingsStore = $SettingsStore;
/**
* An API allowing you to dynamically load styles
* a

View File

@ -46,6 +46,7 @@ const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
@ -154,7 +155,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
<Text variant="text-md/bold" className={cl("name")}>
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
</Text>
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
{plugin.options
? <CogWheel />
: <InfoIcon width="24" height="24" />}

View File

@ -24,6 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes, useAwaiter } from "@utils/misc";
import { relaunch } from "@utils/native";
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
@ -133,7 +134,7 @@ function Updatable(props: CommonProps) {
cancelText: "Not now!",
onConfirm() {
if (needFullRestart)
window.DiscordNative.app.relaunch();
relaunch();
else
location.reload();
r();

View File

@ -26,6 +26,7 @@ import { ErrorCard } from "@components/ErrorCard";
import IpcEvents from "@utils/IpcEvents";
import { Margins } from "@utils/margins";
import { identity, useAwaiter } from "@utils/misc";
import { relaunch } from "@utils/native";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
const cl = classNameFactory("vc-settings-");
@ -100,7 +101,7 @@ function VencordSettings() {
) : (
<React.Fragment>
<Button
onClick={() => window.DiscordNative.app.relaunch()}
onClick={relaunch}
size={Button.Sizes.SMALL}>
Restart Client
</Button>
@ -111,6 +112,7 @@ function VencordSettings() {
Open QuickCSS File
</Button>
<Button
// FIXME: Vencord Desktop support
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}>

View File

@ -21,8 +21,7 @@ import "./settingsStyles.css";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { findByCodeLazy } from "@webpack";
import { Forms, SettingsRouter, Text } from "@webpack/common";
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
import BackupRestoreTab from "./BackupRestoreTab";
import PluginsTab from "./PluginsTab";
@ -32,8 +31,6 @@ import VencordSettings from "./VencordTab";
const cl = classNameFactory("vc-settings-");
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
interface SettingsProps {
tab: string;
}

3
src/globals.d.ts vendored
View File

@ -35,6 +35,8 @@ declare global {
export var IS_WEB: boolean;
export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VENCORD_DESKTOP: boolean;
export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord");
@ -54,6 +56,7 @@ declare global {
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
*/
export var DiscordNative: any;
export var VencordDesktop: any;
interface Window {
webpackChunkdiscord_app: {

109
src/main/index.ts Normal file
View File

@ -0,0 +1,109 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { app, protocol, session } from "electron";
import { join } from "path";
import { getSettings } from "./ipcMain";
import { IS_VANILLA } from "./utils/constants";
import { installExt } from "./utils/extensions";
if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
case "main.js.map":
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
try {
if (getSettings().enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
});
}
if (IS_DISCORD_DESKTOP) {
require("./patcher");
}

View File

@ -28,7 +28,7 @@ import { join } from "path";
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
mkdirSync(SETTINGS_DIR, { recursive: true });
@ -44,6 +44,14 @@ export function readSettings() {
}
}
export function getSettings(): typeof import("@api/settings").Settings {
try {
return JSON.parse(readSettings());
} catch {
return {} as any;
}
}
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {

View File

@ -17,12 +17,11 @@
*/
import { onceDefined } from "@utils/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu, protocol, session } from "electron";
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import { dirname, join } from "path";
import { initIpc } from "./ipcMain";
import { installExt } from "./ipcMain/extensions";
import { readSettings } from "./ipcMain/index";
import { getSettings, initIpc } from "./ipcMain";
import { IS_VANILLA } from "./utils/constants";
console.log("[Vencord] Starting up...");
@ -41,11 +40,8 @@ require.main!.filename = join(asarPath, discordPkg.main);
// @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath);
if (!process.argv.includes("--vanilla")) {
let settings: typeof import("@api/settings").Settings = {} as any;
try {
settings = JSON.parse(readSettings());
} catch { }
if (!IS_VANILLA) {
const settings = getSettings();
// Repatch after host updates on Windows
if (process.platform === "win32") {
@ -116,84 +112,6 @@ if (!process.argv.includes("--vanilla")) {
);
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
try {
if (settings?.enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
});
} else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}

View File

@ -25,7 +25,7 @@ import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import { get } from "../simpleGet";
import { get } from "../utils/simpleGet";
import { calculateHashes, serializeErrors } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
@ -57,6 +57,13 @@ async function calculateGitChanges() {
}));
}
const FILES_TO_DOWNLOAD = [
IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js",
"preload.js",
IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js",
"renderer.css"
];
async function fetchUpdates() {
const release = await githubGet("/releases/latest");
@ -66,7 +73,7 @@ async function fetchUpdates() {
return false;
data.assets.forEach(({ name, browser_download_url }) => {
if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
if (FILES_TO_DOWNLOAD.some(s => name.startsWith(s))) {
PendingUpdates.push([name, browser_download_url]);
}
});
@ -75,8 +82,17 @@ async function fetchUpdates() {
async function applyUpdates() {
await Promise.all(PendingUpdates.map(
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
);
async ([name, data]) => writeFile(
join(
__dirname,
IS_VENCORD_DESKTOP
// vencordDesktopRenderer.js -> renderer.js
? name.replace(/vencordDesktop(\w)/, (_, c) => c.toLowerCase())
: name
),
await get(data)
)
));
PendingUpdates = [];
return true;
}

View File

@ -33,3 +33,5 @@ export const ALLOWED_PROTOCOLS = [
"steam:",
"spotify:"
];
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");

View File

@ -29,7 +29,7 @@ import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms } from "@webpack/common";
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png";
/** List of vencord contributor IDs */
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());

View File

@ -1,82 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
// duplicate values have multiple branches with different types. Just include all to be safe
const nameMap = {
radio: "MenuRadioItem",
separator: "MenuSeparator",
checkbox: "MenuCheckboxItem",
groupstart: "MenuGroup",
control: "MenuControlItem",
compositecontrol: "MenuControlItem",
item: "MenuItem",
customitem: "MenuItem",
};
migratePluginSettings("MenuItemDeobfuscatorAPI", "MenuItemDeobfuscatorApi");
export default definePlugin({
name: "MenuItemDeobfuscatorAPI",
description: "Deobfuscates Discord's Menu Item module",
authors: [Devs.Ven],
patches: [
{
find: '"Menu API',
replacement: {
match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s,
replace: (m, mod) => {
let nicenNames = "";
const redefines = [] as string[];
// if (t.type === m.MenuItem)
const typeCheckRe = /\(.{1,3}\.type===(.{1,5})\)/g;
// push({type:"item"})
const pushTypeRe = /type:"(\w+)"/g;
let typeMatch: RegExpExecArray | null;
// for each if (t.type === ...)
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
// extract the current menu item
const item = typeMatch[1];
// Set the starting index of the second regex to that of the first to start
// matching from after the if
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
// extract the first type: "..."
const type = pushTypeRe.exec(m)?.[1];
if (type && type in nameMap) {
const name = nameMap[type];
nicenNames += `Object.defineProperty(${item},"name",{value:"${name}"});`;
redefines.push(`${name}:${item}`);
}
}
if (redefines.length < 6) {
console.warn("[ApiMenuItemDeobfuscator] Expected to at least remap 6 items, only remapped", redefines.length);
}
// Merge all our redefines with the actual module
return `${nicenNames}Object.assign(${mod},{${redefines.join(",")}});${m}`;
},
},
},
],
});

View File

@ -0,0 +1,38 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "SettingsStoreAPI",
description: "Patches Discord's SettingsStores to expose their group and name",
authors: [Devs.Nuckyz],
patches: [
{
find: '"textAndImages","renderSpoilers"',
replacement: [
{
match: /(?<=INFREQUENT_USER_ACTION.{0,20}),useSetting:function/,
replace: ",settingsStoreApiGroup:arguments[0],settingsStoreApiName:arguments[1]$&"
}
]
}
]
});

View File

@ -0,0 +1,84 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { i18n, React, useStateFromStores } from "@webpack/common";
const cl = classNameFactory("vc-bf-");
const classes = findByPropsLazy("sidebar", "guilds");
const Animations = findByPropsLazy("a", "animated", "useTransition");
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
function Guilds(props: {
className: string;
bfGuildFolders: any[];
}) {
// @ts-expect-error
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props);
const scrollerProps = res.props.children?.props?.children?.[1]?.props;
if (scrollerProps?.children) {
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
if (servers) scrollerProps.children = servers;
}
return res;
}
export default ErrorBoundary.wrap(() => {
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
const fullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
const guilds = document.querySelector(`.${classes.guilds}`);
const visible = !!expandedFolders.size;
const className = cl("folder-sidebar", { fullscreen });
const Sidebar = (
<Guilds
className={classes.guilds}
bfGuildFolders={Array.from(expandedFolders)}
/>
);
if (!guilds || !Settings.plugins.BetterFolders.sidebarAnim)
return visible
? <div className={className}>{Sidebar}</div>
: null;
return (
<Animations.Transition
items={visible}
from={{ width: 0 }}
enter={{ width: guilds.getBoundingClientRect().width }}
leave={{ width: 0 }}
config={{ duration: 200 }}
>
{(style, show) => show && (
<Animations.animated.div style={style} className={className}>
{Sidebar}
</Animations.animated.div>
)}
</Animations.Transition>
);
}, { noop: true });

View File

@ -0,0 +1,17 @@
.vc-bf-folder-sidebar [class*="wrapper-"] > [class*="listItem-"]:first-of-type,
.vc-bf-folder-sidebar [class*="unreadMentionsIndicator"] {
display: none;
}
.vc-bf-folder-sidebar [class*="expandedFolderBackground-"] {
background-color: transparent;
}
.vc-bf-folder-sidebar {
display: flex;
}
.vc-bf-fullscreen {
width: 0 !important;
visibility: hidden;
}

View File

@ -0,0 +1,177 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./betterFolders.css";
import { definePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
const GuildsTree = findLazy(m => m.prototype?.convertToFolder);
const GuildFolderStore = findStoreLazy("SortedGuildStore");
const ExpandedFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
const settings = definePluginSettings({
sidebar: {
type: OptionType.BOOLEAN,
description: "Display servers from folder on dedicated sidebar",
default: true,
},
sidebarAnim: {
type: OptionType.BOOLEAN,
description: "Animate opening the folder sidebar",
default: true,
},
closeAllFolders: {
type: OptionType.BOOLEAN,
description: "Close all folders when selecting a server not in a folder",
default: false,
},
closeAllHomeButton: {
type: OptionType.BOOLEAN,
description: "Close all folders when clicking on the home button",
default: false,
},
closeOthers: {
type: OptionType.BOOLEAN,
description: "Close other folders when opening a folder",
default: false,
},
forceOpen: {
type: OptionType.BOOLEAN,
description: "Force a folder to open when switching to a server of that folder",
default: false,
},
});
export default definePlugin({
name: "BetterFolders",
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
authors: [Devs.juby],
patches: [
{
find: '("guildsnav")',
predicate: () => settings.store.sidebar,
replacement: [
{
match: /(\i)\(\){return \i\(\(0,\i\.jsx\)\("div",{className:\i\(\)\.guildSeparator}\)\)}/,
replace: "$&$self.Separator=$1;"
},
// Folder component patch
{
match: /\i\(\(function\(\i,\i,\i\){var \i=\i\.key;return.+\(\i\)},\i\)}\)\)/,
replace: "arguments[0].bfHideServers?null:$&"
},
// BEGIN Guilds component patch
{
match: /(\i)\.themeOverride,(.{15,25}\(function\(\){var \i=)(\i\.\i\.getGuildsTree\(\))/,
replace: "$1.themeOverride,bfPatch=$1.bfGuildFolders,$2bfPatch?$self.getGuildsTree(bfPatch,$3):$3"
},
{
match: /return(\(0,\i\.jsx\))(\(\i,{)(folderNode:\i,setNodeRef:\i\.setNodeRef,draggable:!0,.+},\i\.id\));case/,
replace: "var bfHideServers=typeof bfPatch==='undefined',folder=$1$2bfHideServers,$3;return !bfHideServers&&arguments[1]?[$1($self.Separator,{}),folder]:folder;case"
},
// END
{
match: /\("guildsnav"\);return\(0,\i\.jsx\)\(.{1,6},{navigator:\i,children:\(0,\i\.jsx\)\(/,
replace: "$&$self.Guilds="
}
]
},
{
find: "APPLICATION_LIBRARY,render",
predicate: () => settings.store.sidebar,
replacement: {
match: /(\(0,\i\.jsx\))\(\i\..,{className:\i\(\)\.guilds,themeOverride:\i}\)/,
replace: "$&,$1($self.FolderSideBar,{})"
}
},
{
find: '("guildsnav")',
predicate: () => settings.store.closeAllHomeButton,
replacement: {
match: ",onClick:function(){if(!__OVERLAY__){",
replace: "$&$self.closeFolders();"
}
}
],
settings,
start() {
const getGuildFolder = (id: string) => GuildFolderStore.guildFolders.find(f => f.guildIds.includes(id));
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onSwitch = data => {
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
return;
if (this.lastGuildId !== data.guildId) {
this.lastGuildId = data.guildId;
const guildFolder = getGuildFolder(data.guildId);
if (guildFolder?.folderId) {
if (settings.store.forceOpen && !ExpandedFolderStore.isFolderExpanded(guildFolder.folderId))
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
} else if (settings.store.closeAllFolders)
this.closeFolders();
}
});
FluxDispatcher.subscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder = e => {
if (settings.store.closeOthers && !this.dispatching)
FluxDispatcher.wait(() => {
const expandedFolders = ExpandedFolderStore.getExpandedFolders();
if (expandedFolders.size > 1) {
this.dispatching = true;
for (const id of expandedFolders) if (id !== e.folderId)
FolderUtils.toggleGuildFolderExpand(id);
this.dispatching = false;
}
});
});
},
stop() {
FluxDispatcher.unsubscribe("CHANNEL_SELECT", this.onSwitch);
FluxDispatcher.unsubscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder);
},
FolderSideBar,
getGuildsTree(folders, oldTree) {
const tree = new GuildsTree();
tree.root.children = oldTree.root.children.filter(e => folders.includes(e.id));
tree.nodes = folders.map(id => oldTree.nodes[id]);
return tree;
},
closeFolders() {
for (const id of ExpandedFolderStore.getExpandedFolders())
FolderUtils.toggleGuildFolderExpand(id);
},
});

View File

@ -44,7 +44,7 @@ export default definePlugin({
match: /"(?:username|dot)"===\i(?!\.\i)/g,
replace: "true",
},
},
}
],
options: {

View File

@ -17,6 +17,7 @@
*/
import { Devs } from "@utils/constants";
import { relaunch } from "@utils/native";
import definePlugin from "@utils/types";
import * as Webpack from "@webpack";
import { extract, filters, findAll, search } from "@webpack";
@ -71,13 +72,14 @@ export default definePlugin({
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode),
findAllByCode: (code: string) => findAll(filters.byCode(code)),
findStore: newFindWrapper(filters.byStoreName),
PluginsApi: Vencord.Plugins,
plugins: Vencord.Plugins.plugins,
React,
Settings: Vencord.Settings,
Api: Vencord.Api,
reload: () => location.reload(),
restart: IS_WEB ? WEB_ONLY("restart") : window.DiscordNative.app.relaunch
restart: IS_WEB ? WEB_ONLY("restart") : relaunch
};
},

View File

@ -1,105 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
import { findOption } from "@api/Commands/commandHelpers";
import { ApplicationCommandInputType } from "@api/Commands/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByCode, findByProps } from "@webpack";
const DRAFT_TYPE = 0;
export default definePlugin({
name: "CorruptMp4s",
description: "Create corrupt mp4s with extremely high or negative duration",
authors: [Devs.Ven],
dependencies: ["CommandsAPI"],
commands: [{
name: "corrupt",
description: "Create a corrupt mp4 with extremely high or negative duration",
inputType: ApplicationCommandInputType.BUILT_IN,
options: [
{
name: "mp4",
description: "the video to corrupt",
type: ApplicationCommandOptionType.ATTACHMENT,
required: true
},
{
name: "kind",
description: "the kind of corruption",
type: ApplicationCommandOptionType.STRING,
choices: [
{
name: "infinite",
value: "infinite",
label: "Very high duration"
},
{
name: "negative",
value: "negative",
label: "Negative duration"
}
]
}
],
execute: async (args, ctx) => {
const UploadStore = findByProps("getUploads");
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
const video = upload?.item?.file as File | undefined;
if (video?.type !== "video/mp4")
return void sendBotMessage(ctx.channel.id, {
content: "Please upload a mp4 file"
});
const corruption = findOption<string>(args, "kind", "infinite");
const buf = new Uint8Array(await video.arrayBuffer());
let found = false;
// adapted from https://github.com/GeopJr/exorcism/blob/c9a12d77ccbcb49c987b385eafae250906efc297/src/App.svelte#L41-L48
for (let i = 0; i < buf.length; i++) {
if (buf[i] === 0x6d && buf[i + 1] === 0x76 && buf[i + 2] === 0x68 && buf[i + 3] === 0x64) {
let start = i + 18;
buf[start++] = 0x00;
buf[start++] = 0x01;
buf[start++] = corruption === "negative" ? 0xff : 0x7f;
buf[start++] = 0xff;
buf[start++] = 0xff;
buf[start++] = corruption === "negative" ? 0xf0 : 0xff;
found = true;
break;
}
}
if (!found) {
return void sendBotMessage(ctx.channel.id, {
content: "Could not find signature. Is this even a mp4?"
});
}
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
const file = new File([buf], newName, { type: "video/mp4" });
setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10);
}
}]
});

View File

@ -161,7 +161,11 @@ function initWs(isManual = false) {
return reply("Expected exactly one 'find' matches, found " + keys.length);
const mod = candidates[keys[0]];
let src = String(mod.original ?? mod);
let src = String(mod.original ?? mod).replaceAll("\n", "");
if (src.startsWith("function(")) {
src = "0," + src;
}
let i = 0;

View File

@ -238,7 +238,7 @@ export default definePlugin({
name: "EmoteCloner",
description: "Adds a Clone context menu item to emotes to clone them your own server",
authors: [Devs.Ven, Devs.Nuckyz],
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
dependencies: ["ContextMenuAPI"],
start() {
addContextMenuPatch("message", messageContextMenuPatch);

View File

@ -0,0 +1,145 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// This plugin is a port from Alyxia's Vendetta plugin
import { definePluginSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Forms } from "@webpack/common";
import { User } from "discord-types/general";
import virtualMerge from "virtual-merge";
interface UserProfile extends User {
themeColors?: Array<number>;
}
interface Colors {
primary: number;
accent: number;
}
function encode(primary: number, accent: number): string {
const message = `[#${primary.toString(16).padStart(6, "0")},#${accent.toString(16).padStart(6, "0")}]`;
const padding = "";
const encoded = Array.from(message)
.map(x => x.codePointAt(0))
.filter(x => x! >= 0x20 && x! <= 0x7f)
.map(x => String.fromCodePoint(x! + 0xe0000))
.join("");
return (padding || "") + " " + encoded;
}
// Courtesy of Cynthia.
function decode(bio: string): Array<number> | null {
if (bio == null) return null;
const colorString = bio.match(
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u,
);
if (colorString != null) {
const parsed = [...colorString[0]]
.map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000))
.join("");
const colors = parsed
.substring(1, parsed.length - 1)
.split(",")
.map(x => parseInt(x.replace("#", "0x"), 16));
return colors;
} else {
return null;
}
}
const settings = definePluginSettings({
nitroFirst: {
description: "Default color source if both are present",
type: OptionType.SELECT,
options: [
{ label: "Nitro colors", value: true, default: true },
{ label: "Fake colors", value: false },
]
}
});
export default definePlugin({
name: "FakeProfileThemes",
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding.",
authors: [Devs.Alyxia, Devs.Remty],
patches: [
{
find: "getUserProfile=",
replacement: {
match: /(?<=getUserProfile=function\(\i\){return )(\i\[\i\])/,
replace: "$self.colorDecodeHook($1)"
}
}, {
find: ".USER_SETTINGS_PROFILE_THEME_ACCENT",
replacement: {
match: /RESET_PROFILE_THEME}\)(?<=},color:(\i).+?},color:(\i).+?)/,
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
}
}
],
settingsAboutComponent: () => (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
<Forms.FormText>
After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins. <br />
To set your own colors:
<ul>
<li> go to your profile settings</li>
<li> choose your own colors in the Nitro preview</li>
<li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li>
</ul><br />
<b>Please note:</b> if you are using a theme which hides nitro upsells, you should disable it temporarily to set colors.
</Forms.FormText>
</Forms.FormSection>),
settings,
colorDecodeHook(user: UserProfile) {
if (user) {
// don't replace colors if already set with nitro
if (settings.store.nitroFirst && user.themeColors) return user;
const colors = decode(user.bio);
if (colors) {
return virtualMerge(user, {
premiumType: 2,
themeColors: colors
});
}
}
return user;
},
addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) {
return <Button
onClick={() => {
const colorString = encode(primary, accent);
copyWithToast(colorString);
}}
color={Button.Colors.PRIMARY}
size={Button.Sizes.XLARGE}
className={Margins.left16}
>Copy 3y3
</Button >;
}, { noop: true }),
});

View File

@ -0,0 +1,85 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getSettingStoreLazy } from "@api/SettingsStore";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack";
import style from "./style.css?managed";
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame");
const Button = findByCodeLazy("Button.Sizes.NONE,disabled:");
function makeIcon(showCurrentGame?: boolean) {
return function () {
return (
<svg
width="24"
height="24"
viewBox="0 96 960 960"
>
<path fill="currentColor" d="M182 856q-51 0-79-35.5T82 734l42-300q9-60 53.5-99T282 296h396q60 0 104.5 39t53.5 99l42 300q7 51-21 86.5T778 856q-21 0-39-7.5T706 826l-90-90H344l-90 90q-15 15-33 22.5t-39 7.5Zm498-240q17 0 28.5-11.5T720 576q0-17-11.5-28.5T680 536q-17 0-28.5 11.5T640 576q0 17 11.5 28.5T680 616Zm-80-120q17 0 28.5-11.5T640 456q0-17-11.5-28.5T600 416q-17 0-28.5 11.5T560 456q0 17 11.5 28.5T600 496ZM310 616h60v-70h70v-60h-70v-70h-60v70h-70v60h70v70Z" />
{!showCurrentGame && <line x1="920" y1="280" x2="40" y2="880" stroke="var(--status-danger)" stroke-width="80" />}
</svg>
);
};
}
function GameActivityToggleButton() {
const showCurrentGame = ShowCurrentGame?.useSetting();
return (
<Button
tooltipText="Toggle Game Activity"
icon={makeIcon(showCurrentGame)}
role="switch"
aria-checked={!showCurrentGame}
onClick={() => ShowCurrentGame?.updateSetting(old => !old)}
/>
);
}
export default definePlugin({
name: "GameActivityToggle",
description: "Adds a button next to the mic and deafen button to toggle game activity.",
authors: [Devs.Nuckyz],
dependencies: ["SettingsStoreAPI"],
patches: [
{
find: ".Messages.ACCOUNT_SPEAKING_WHILE_MUTED",
replacement: {
match: /this\.renderNameZone\(\).+?children:\[/,
replace: "$&$self.GameActivityToggleButton(),"
}
}
],
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
});

View File

@ -0,0 +1,3 @@
[class*="withTagAsButton"] {
min-width: 88px;
}

View File

@ -76,7 +76,7 @@ export default definePlugin({
name: "MessageLogger",
description: "Temporarily logs deleted and edited messages.",
authors: [Devs.rushii, Devs.Ven],
dependencies: ["ContextMenuAPI", "MenuItemDeobfuscatorAPI"],
dependencies: ["ContextMenuAPI"],
start() {
addDeleteStyle();
@ -209,6 +209,11 @@ export default definePlugin({
" m" +
")" +
".update($3"
},
{
// fix up key (edit last message) attempting to edit a deleted message
match: /(?<=getLastEditableMessage=.{0,200}\.find\(\(function\((\i)\)\{)return/,
replace: "return !$1.deleted &&"
}
]
},

View File

@ -111,7 +111,7 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
}
function getNextMessage(isUp: boolean, isReply: boolean) {
let messages: Message[] = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
let messages: Array<Message & { deleted?: boolean; }> = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
if (!isReply) { // we are editing so only include own
const meId = UserStore.getCurrentUser().id;
messages = messages.filter(m => m.author.id === meId);
@ -121,11 +121,18 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
? Math.min(messages.length - 1, i + 1)
: Math.max(-1, i - 1);
const findNextNonDeleted = (i: number) => {
do {
i = mutate(i);
} while (i !== -1 && messages[messages.length - i - 1]?.deleted === true);
return i;
};
let i: number;
if (isReply)
replyIdx = i = mutate(replyIdx);
replyIdx = i = findNextNonDeleted(replyIdx);
else
editIdx = i = mutate(editIdx);
editIdx = i = findNextNonDeleted(editIdx);
return i === - 1 ? undefined : messages[messages.length - i - 1];
}

View File

@ -62,10 +62,10 @@ export default definePlugin({
renderReadAllButton: () => <ReadAllButton />,
start() {
addServerListElement(ServerListRenderPosition.In, this.renderReadAllButton);
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
},
stop() {
removeServerListElement(ServerListRenderPosition.In, this.renderReadAllButton);
removeServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
}
});

View File

@ -76,7 +76,7 @@ export default definePlugin({
name: "ReverseImageSearch",
description: "Adds ImageSearch to image context menus",
authors: [Devs.Ven, Devs.Nuckyz],
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
dependencies: ["ContextMenuAPI"],
patches: [
{
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",

View File

@ -168,6 +168,7 @@ export default definePlugin({
get additionalInfo() {
if (IS_DEV) return " (Dev)";
if (IS_WEB) return " (Web)";
if (IS_VENCORD_DESKTOP) return " (Vencord Desktop)";
if (IS_STANDALONE) return " (Standalone)";
return "";
},

View File

@ -22,8 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { debounce } from "@utils/debounce";
import { classes, copyWithToast, LazyComponent } from "@utils/misc";
import { filters, find } from "@webpack";
import { classes, copyWithToast } from "@utils/misc";
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { SpotifyStore, Track } from "./SpotifyStore";
@ -79,7 +78,7 @@ function CopyContextMenu({ name, path }: { name: string; path: string; }) {
const openId = `spotify-open-${name}`;
return (
<Menu.ContextMenu
<Menu.Menu
navId={`spotify-${name}-menu`}
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label={`Spotify ${name} Menu`}
@ -96,7 +95,7 @@ function CopyContextMenu({ name, path }: { name: string; path: string; }) {
label={`Open ${name} in Spotify`}
action={() => SpotifyStore.openExternal(path)}
/>
</Menu.ContextMenu>
</Menu.Menu>
);
}
@ -154,11 +153,6 @@ const seek = debounce((v: number) => {
SpotifyStore.seek(v);
});
const Slider = LazyComponent(() => {
const filter = filters.byCode("sliderContainer");
return find(m => m.render && filter(m.render));
});
function SeekBar() {
const { duration } = SpotifyStore.track!;
@ -190,7 +184,7 @@ function SeekBar() {
>
{msToHuman(position)}
</Forms.FormText>
<Slider
<Menu.MenuSliderControl
minValue={0}
maxValue={duration}
value={position}
@ -217,7 +211,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
const volume = useStateFromStores([SpotifyStore], () => SpotifyStore.volume);
return (
<Menu.ContextMenu
<Menu.Menu
navId="spotify-album-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Spotify Album Menu"
@ -240,7 +234,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
key="spotify-volume"
label="Volume"
control={(props, ref) => (
<Slider
<Menu.MenuSliderControl
{...props}
ref={ref}
value={volume}
@ -250,7 +244,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
/>
)}
/>
</Menu.ContextMenu>
</Menu.Menu>
);
}
@ -372,10 +366,10 @@ export function Player() {
return (
<ErrorBoundary fallback={() => (
<>
<Forms.FormText>Failed to render Spotify Modal :(</Forms.FormText>
<Forms.FormText>Check the console for errors</Forms.FormText>
</>
<div className="vc-spotify-fallback">
<p>Failed to render Spotify Modal :(</p>
<p >Check the console for errors</p>
</div>
)}>
<div id={cl("player")}>
<Info track={track} />

View File

@ -39,7 +39,6 @@ export default definePlugin({
name: "SpotifyControls",
description: "Spotify Controls",
authors: [Devs.Ven, Devs.afn, Devs.KraXen72],
dependencies: ["MenuItemDeobfuscatorAPI"],
options: {
hoverControls: {
description: "Show controls on hover",

View File

@ -191,3 +191,8 @@
.vc-spotify-time-right {
right: 0;
}
.vc-spotify-fallback {
padding: 0.5em;
color: var(--text-normal);
}

View File

@ -33,7 +33,7 @@ const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
export default definePlugin({
name: "SupportHelper",
required: true,
description: "Helps me provide support to you",
description: "Helps us provide support to you",
authors: [Devs.Ven],
commands: [{

View File

@ -35,8 +35,6 @@ export default definePlugin({
authors: [Devs.Ven],
description: "Makes Avatars/Banners in user profiles clickable, and adds Guild Context Menu Entries to View Banner/Icon.",
dependencies: ["MenuItemDeobfuscatorAPI"],
openImage(url: string) {
const u = new URL(url);
u.searchParams.set("size", "512");

View File

@ -54,7 +54,9 @@ if (location.protocol !== "data:") {
document.getElementById("vencord-css-core")!.textContent = readFileSync(rendererCss, "utf-8");
});
}
require(process.env.DISCORD_PRELOAD!);
if (process.env.DISCORD_PRELOAD)
require(process.env.DISCORD_PRELOAD);
} else {
// Monaco Popout
contextBridge.exposeInMainWorld("setCss", debounce(s => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, s)));

View File

@ -226,6 +226,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "TheKodeToad",
id: 706152404072267788n
},
juby: {
name: "Juby210",
id: 324622488644616195n
},
Alyxia: {
name: "Alyxia Sother",
id: 952185386350829688n
},
Remty: {
name: "Remty",
id: 335055032204656642n
},
skyevg: {
name: "skyevg",
id: 1090310844283363348n

24
src/utils/native.ts Normal file
View File

@ -0,0 +1,24 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function relaunch() {
if (IS_DISCORD_DESKTOP)
window.DiscordNative.app.relaunch();
else
window.VencordDesktop.app.relaunch();
}

View File

@ -47,7 +47,9 @@ export async function downloadSettingsBackup() {
const backup = await exportSettings();
const data = new TextEncoder().encode(backup);
if (IS_WEB) {
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(data, filename);
} else {
const file = new File([data], filename, { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
@ -59,8 +61,6 @@ export async function downloadSettingsBackup() {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
});
} else {
DiscordNative.fileManager.saveWithDialog(data, filename);
}
}
@ -77,7 +77,24 @@ const toastFailure = (err: any) => Toasts.show({
});
export async function uploadSettingsBackup(showToast = true): Promise<void> {
if (IS_WEB) {
if (IS_DISCORD_DESKTOP) {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
{ name: "Vencord Settings Backup", extensions: ["json"] },
{ name: "all", extensions: ["*"] }
]
});
if (file) {
try {
await importSettings(new TextDecoder().decode(file.data));
if (showToast) toastSuccess();
} catch (err) {
new Logger("SettingsSync").error(err);
if (showToast) toastFailure(err);
}
}
} else {
const input = document.createElement("input");
input.type = "file";
input.style.display = "none";
@ -102,22 +119,5 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
document.body.appendChild(input);
input.click();
setImmediate(() => document.body.removeChild(input));
} else {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
{ name: "Vencord Settings Backup", extensions: ["json"] },
{ name: "all", extensions: ["*"] }
]
});
if (file) {
try {
await importSettings(new TextDecoder().decode(file.data));
if (showToast) toastSuccess();
} catch (err) {
new Logger("SettingsSync").error(err);
if (showToast) toastFailure(err);
}
}
}
}

View File

@ -20,6 +20,7 @@ import gitHash from "~git-hash";
import IpcEvents from "./IpcEvents";
import Logger from "./Logger";
import { relaunch } from "./native";
import { IpcRes } from "./types";
export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white");
@ -90,8 +91,10 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev =
if (wantsUpdate) {
await update();
const needFullRestart = await rebuild();
if (needFullRestart) DiscordNative.app.relaunch();
else location.reload();
if (needFullRestart)
relaunch();
else
location.reload();
}
}
} catch (err) {

View File

@ -17,40 +17,37 @@
*/
// eslint-disable-next-line path-alias/no-relative
import { filters, findByPropsLazy } from "../webpack";
import { filters, findByPropsLazy, waitFor } from "@webpack";
import { waitForComponent } from "./internal";
import * as t from "./types/components";
export const Forms = {
FormTitle: waitForComponent<t.FormTitle>("FormTitle", filters.byCode("errorSeparator")),
FormSection: waitForComponent<t.FormSection>("FormSection", filters.byCode("titleClassName", "sectionTitle")),
FormDivider: waitForComponent<t.FormDivider>("FormDivider", m => {
if (typeof m !== "function") return false;
const s = m.toString();
return s.length < 200 && s.includes(".divider");
}),
FormText: waitForComponent<t.FormText>("FormText", m => m.Types?.INPUT_PLACEHOLDER),
export let Forms = {} as {
FormTitle: t.FormTitle,
FormSection: t.FormSection,
FormDivider: t.FormDivider,
FormText: t.FormText,
};
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY && m.defaultProps);
export const Button = waitForComponent<t.Button>("Button", ["Hovers", "Looks", "Sizes"]);
export const Switch = waitForComponent<t.Switch>("Switch", filters.byCode("tooltipNote", "ringTarget"));
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", filters.byCode("shouldShowTooltip:!1", "clickableOnMobile||"));
export let Card: t.Card;
export let Button: t.Button;
export let Switch: t.Switch;
export let Tooltip: t.Tooltip;
export let TextInput: t.TextInput;
export let TextArea: t.TextArea;
export let Text: t.Text;
export let Select: t.Select;
export let SearchableSelect: t.SearchableSelect;
export let Slider: t.Slider;
export let ButtonLooks: t.ButtonLooks;
export let TabBar: any;
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
export const TextInput = waitForComponent<t.TextInput>("TextInput", ["defaultProps", "Sizes", "contextType"]);
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.byCode("handleSetRef", "textArea"));
export const Text = waitForComponent<t.Text>("Text", m => {
if (typeof m !== "function") return false;
const s = m.toString();
return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white"));
});
export const Select = waitForComponent<t.Select>("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
const searchableSelectFilter = filters.byCode("autoFocus", ".Messages.SELECT");
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", m =>
m.render && searchableSelectFilter(m.render)
);
export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>;
export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED");
waitFor("FormItem", m => {
({ Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar } = m);
Forms = m;
});

View File

@ -16,32 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { proxyLazy } from "@utils/proxyLazy";
// eslint-disable-next-line path-alias/no-relative
import { filters, mapMangledModule, mapMangledModuleLazy } from "../webpack";
import { filters, mapMangledModuleLazy, waitFor } from "../webpack";
import type * as t from "./types/menu";
export const Menu: t.Menu = proxyLazy(() => {
const hasDeobfuscator = Vencord.Settings.plugins.MenuItemDeobfuscatorAPI.enabled;
const menuItems = ["MenuSeparator", "MenuGroup", "MenuItem", "MenuCheckboxItem", "MenuRadioItem", "MenuControlItem"];
export let Menu = {} as t.Menu;
const map = mapMangledModule("♫ ⊂(。◕‿‿◕。⊂) ♪", {
ContextMenu: filters.byCode("getContainerProps"),
...Object.fromEntries((hasDeobfuscator ? menuItems : []).map(s => [s, (m: any) => m.name === s]))
}) as t.Menu;
if (!hasDeobfuscator) {
for (const m of menuItems)
Object.defineProperty(map, m, {
get() {
throw new Error("MenuItemDeobfuscator must be enabled to use this.");
}
});
}
return map;
});
waitFor("MenuItem", m => Menu = m);
export const ContextMenu: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN"', {
open: filters.byCode("stopPropagation"),

View File

@ -21,7 +21,7 @@ import type { ComponentType, CSSProperties, PropsWithChildren, UIEvent } from "r
type RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>;
export interface Menu {
ContextMenu: RC<{
Menu: RC<{
navId: string;
onClose(): void;
className?: string;
@ -49,19 +49,21 @@ export interface Menu {
id: string;
interactive?: boolean;
}>;
// TODO: Type me
MenuSliderControl: RC<any>;
}
export interface ContextMenuApi {
close(): void;
open(
event: UIEvent,
render?: Menu["ContextMenu"],
render?: Menu["Menu"],
options?: { enableSpellCheck?: boolean; },
renderLazy?: () => Promise<Menu["ContextMenu"]>
renderLazy?: () => Promise<Menu["Menu"]>
): void;
openLazy(
event: UIEvent,
renderLazy?: () => Promise<Menu["ContextMenu"]>,
renderLazy?: () => Promise<Menu["Menu"]>,
options?: { enableSpellCheck?: boolean; }
): void;
}

View File

@ -30,6 +30,7 @@ export interface FluxDispatcher {
isDispatching(): boolean;
subscribe(event: FluxEvents, callback: (data: any) => void): void;
unsubscribe(event: FluxEvents, callback: (data: any) => void): void;
wait(callback: () => void): void;
}
export type Parser = Record<

View File

@ -67,7 +67,7 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
instance.pop();
}
if (IS_DEV && !IS_WEB) {
if (IS_DEV && IS_DISCORD_DESKTOP) {
var devToolsOpen = false;
// At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed
setTimeout(() => {
@ -109,6 +109,8 @@ export const find = traceFunction("find", function find(filter: FilterFn, getDef
if (!isWaitFor) {
const err = new Error("Didn't find module matching this filter");
if (IS_DEV) {
logger.error(err);
logger.error(filter);
if (!devToolsOpen)
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
throw err;