Compare commits

...

31 Commits

Author SHA1 Message Date
V
9b987d1e56 Bump to v1.4.6 2023-09-05 21:25:16 +02:00
V
774318d583 ViewRaw: Update description 2023-09-05 21:24:43 +02:00
V
7d954f9ade ViewRaw: Fix ugly copy icon & context menu position 2023-09-05 21:20:13 +02:00
Luna
860d6edc7b HideAttachments: allow hiding stickers (#13)
Self explanatory

Reviewed-on: https://codeberg.org/Ven/cord/pulls/13
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-05 18:56:28 +00:00
Rini
223b0366c6 pinDMs: fix alt+shift+up (#25)
applies the same fix as alt+up/down to the jump to unread one

confirmed non-explosion of shc

Reviewed-on: https://codeberg.org/Ven/cord/pulls/25
Co-authored-by: Rini <rini@rinici.de>
Co-committed-by: Rini <rini@rinici.de>
2023-09-05 18:51:22 +00:00
Luna
69cb7593eb View Raw on more objects (#27)
Shows view raw on guilds, channels, and users

Reviewed-on: https://codeberg.org/Ven/cord/pulls/27
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-05 18:49:57 +00:00
V
8614e17633 Fix some plugins wrongly displaying the settings cog 2023-09-05 20:10:42 +02:00
Ryan Cao
aecd9d8fda feat(memberCount): format count according to user locale (#1679) 2023-09-05 19:46:33 +02:00
Hugo C
faeb4fb585 previewMessage: Hide the button once a message is sent (#1692)
Co-authored-by: V <vendicated@riseup.net>
2023-09-05 19:45:44 +02:00
dolfies
0d18b44ba7 feat(plugin): ShowTimeouts (#1687)
Co-authored-by: TheKodeToad <TheKodeToad@proton.me>
Co-authored-by: V <vendicated@riseup.net>
2023-09-05 19:42:35 +02:00
omahs
d671bd65ac fix typos (#1703) 2023-09-05 19:40:32 +02:00
AutumnVN
8e952c630b MessageLogger: fix ignore by id doesn't ignore edited (#1705) 2023-09-05 19:39:22 +02:00
V
2c758ccdf8 new plugin: ServerProfile (#1704)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2023-09-05 18:36:14 +02:00
Nuckyz
c165725297 Fix OpenInApp patch 2023-09-02 03:03:50 -03:00
Nuckyz
29fbe3701a Fix CrashHandler patch 2023-09-02 03:03:31 -03:00
V
0b7c0e9587 new plugin: themeAttributes (#1686) 2023-08-31 01:49:53 +02:00
Cynthia Foxwell
d88524e8cf ShowMeYourName: Fix username being wrong on slash commands (#1690)
Co-authored-by: V <vendicated@riseup.net>
2023-08-31 01:01:29 +02:00
Hugo C
d6efd99849 messageLinkEmbeds: fix AutoModEmbed on compact mode (#1691)
Co-authored-by: V <vendicated@riseup.net>
2023-08-31 01:00:16 +02:00
ruukulada
fe6be987fd gameActivityToggle: Fix disabled SVG look (#1696) 2023-08-31 00:53:05 +02:00
V
d688075c0a Update Screenshot 2023-08-29 01:12:47 +02:00
V
c752be45b2 fix(security - ipc): do not expose sender 2023-08-25 14:57:04 +02:00
V
07c1f5eed1 Fix theme upload on crapOs 2023-08-25 14:37:50 +02:00
V
4df01b1e62 ci: fix generatePluginList 2023-08-25 14:33:33 +02:00
AutumnVN
ebe10d3fad customRPC fixes (#1666)
Co-authored-by: V <vendicated@riseup.net>
2023-08-25 14:30:12 +02:00
Adrian Castro
eca4af829f feat: add copyUserURLs plugin (#1683)
Co-authored-by: V <vendicated@riseup.net>
2023-08-25 14:24:58 +02:00
CanadaHonk
60458cdf1f Win32Updater: use before-quit event instead of patching updater (#1668) 2023-08-25 14:21:27 +02:00
Sefa Eyeoglu
714d87241c build: allow overriding git details & disabling updating (#1677)
Co-authored-by: V <vendicated@riseup.net>
2023-08-25 14:15:02 +02:00
Elliott Tallis
f628aa7a70 ClearURLs: Add si parameter on youtu.be (#1681) 2023-08-25 14:03:58 +02:00
megumin
0f0282551d 🗿 but HD (#1678) 2023-08-25 14:02:10 +02:00
V
0335e1ca59 new plugin FixSpotifyEmbeds: less loud embeds 2023-08-21 16:59:32 +02:00
V
817f0f7473 [skip ci] fix getPluginTarget 2023-08-20 15:14:28 +02:00
47 changed files with 836 additions and 189 deletions

View File

@ -65,7 +65,7 @@ Also pay attention to the following:
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`, - Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
`var .{1,2}=([^;]+);` `var .{1,2}=([^;]+);`
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters - If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
- Additionally, as you might have noticed, all of the appove approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically - Additionally, as you might have noticed, all of the above approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
#### "replace" #### "replace"

View File

@ -4,8 +4,7 @@
The cutest Discord client mod The cutest Discord client mod
![](https://user-images.githubusercontent.com/45497981/235015332-0453d3eb-1da6-4601-963e-ef5e454123a1.png) ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334)
*A screenshot of Vencord featuring the [ClearVision-v6 theme](https://github.com/ClearVision/ClearVision-v6) (Vencord does not come with it pre-installed, it is only an example)*
## Features ## Features

View File

@ -63,7 +63,7 @@ Then fully close Discord from your taskbar or task manager, and restart it. Venc
If you're using Discord already, go into the `Updater` tab in settings. If you're using Discord already, go into the `Updater` tab in settings.
Sometimes it may be neccessary to manually update if the GUI updater fails. Sometimes it may be necessary to manually update if the GUI updater fails.
To pull latest changes: To pull latest changes:

View File

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.4.5", "version": "1.4.6",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {

View File

@ -19,13 +19,14 @@
import esbuild from "esbuild"; import esbuild from "esbuild";
import { commonOpts, globPlugins, isStandalone, VERSION, watch } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
const defines = { const defines = {
IS_STANDALONE: isStandalone, IS_STANDALONE: isStandalone,
IS_DEV: JSON.stringify(watch), IS_DEV: JSON.stringify(watch),
IS_UPDATER_DISABLED: updaterDisabled,
VERSION: JSON.stringify(VERSION), VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP: Date.now(), BUILD_TIMESTAMP,
}; };
if (defines.IS_STANDALONE === "false") if (defines.IS_STANDALONE === "false")
// If this is a local build (not standalone), optimise // If this is a local build (not standalone), optimise

View File

@ -24,7 +24,7 @@ import { readFileSync } from "fs";
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises"; import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { commonOpts, globPlugins, VERSION, watch } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
/** /**
* @type {esbuild.BuildOptions} * @type {esbuild.BuildOptions}
@ -46,8 +46,9 @@ const commonOptions = {
IS_DEV: JSON.stringify(watch), IS_DEV: JSON.stringify(watch),
IS_DISCORD_DESKTOP: "false", IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "false", IS_VESKTOP: "false",
IS_UPDATER_DISABLED: "true",
VERSION: JSON.stringify(VERSION), VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP: Date.now(), BUILD_TIMESTAMP,
} }
}; };

View File

@ -30,15 +30,18 @@ import PackageJSON from "../../package.json" assert { type: "json" };
import { getPluginTarget } from "../utils.mjs"; import { getPluginTarget } from "../utils.mjs";
export const VERSION = PackageJSON.version; export const VERSION = PackageJSON.version;
export const BUILD_TIMESTAMP = Date.now(); // https://reproducible-builds.org/docs/source-date-epoch/
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
export const watch = process.argv.includes("--watch"); export const watch = process.argv.includes("--watch");
export const isStandalone = JSON.stringify(process.argv.includes("--standalone")); export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim(); export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
export const banner = { export const banner = {
js: ` js: `
// Vencord ${gitHash} // Vencord ${gitHash}
// Standalone: ${isStandalone} // Standalone: ${isStandalone}
// Platform: ${isStandalone === "false" ? process.platform : "Universal"} // Platform: ${isStandalone === "false" ? process.platform : "Universal"}
// Updater disabled: ${updaterDisabled}
`.trim() `.trim()
}; };
@ -133,11 +136,14 @@ export const gitRemotePlugin = {
namespace: "git-remote", path: args.path namespace: "git-remote", path: args.path
})); }));
build.onLoad({ filter, namespace: "git-remote" }, async () => { build.onLoad({ filter, namespace: "git-remote" }, async () => {
let remote = process.env.VENCORD_REMOTE;
if (!remote) {
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" }); const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
const remote = res.stdout.trim() remote = res.stdout.trim()
.replace("https://github.com/", "") .replace("https://github.com/", "")
.replace("git@github.com:", "") .replace("git@github.com:", "")
.replace(/.git$/, ""); .replace(/.git$/, "");
}
return { contents: `export default "${remote}"` }; return { contents: `export default "${remote}"` };
}); });

View File

@ -161,7 +161,7 @@ async function parseFile(fileName: string) {
const target = getPluginTarget(fileName); const target = getPluginTarget(fileName);
if (target) { if (target) {
if (!["web", "discordDesktop", "vencordDesktop", "dev"].includes(target)) throw fail(`invalid target ${target}`); if (!["web", "discordDesktop", "vencordDesktop", "desktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
data.target = target as any; data.target = target as any;
} }

View File

@ -22,7 +22,7 @@
*/ */
export function getPluginTarget(filePath) { export function getPluginTarget(filePath) {
const pathParts = filePath.split(/[/\\]/); const pathParts = filePath.split(/[/\\]/);
if (/^index\.tsx?$/.test(filePath.at(-1))) pathParts.pop(); if (/^index\.tsx?$/.test(pathParts.at(-1))) pathParts.pop();
const identifier = pathParts.at(-1).replace(/\.tsx?$/, ""); const identifier = pathParts.at(-1).replace(/\.tsx?$/, "");
const identiferBits = identifier.split("."); const identiferBits = identifier.split(".");

View File

@ -48,7 +48,7 @@ export default {
}, },
addThemeChangeListener(cb: () => void) { addThemeChangeListener(cb: () => void) {
ipcRenderer.on(IpcEvents.THEME_UPDATE, cb); ipcRenderer.on(IpcEvents.THEME_UPDATE, () => cb());
}, },
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS), openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),

View File

@ -23,7 +23,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { LazyComponent } from "@utils/react"; import { LazyComponent } from "@utils/react";
import { OptionType, Plugin } from "@utils/types"; import { OptionType, Plugin } from "@utils/types";
@ -89,7 +89,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const canSubmit = () => Object.values(errors).every(e => !e); const canSubmit = () => Object.values(errors).every(e => !e);
const hasSettings = Boolean(pluginSettings && plugin.options); const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
React.useEffect(() => { React.useEffect(() => {
enableStyle(hideBotTagStyle); enableStyle(hideBotTagStyle);

View File

@ -33,12 +33,11 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
const isValid = option.isValid?.call(definedSettings, newValue) ?? true; const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else { else setError(null);
setError(null);
setState(newValue); setState(newValue);
onChange(newValue); onChange(newValue);
} }
}
return ( return (
<Forms.FormSection> <Forms.FormSection>

View File

@ -28,7 +28,7 @@ import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList"; import { ChangeList } from "@utils/ChangeList";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { openModalLazy } from "@utils/modal"; import { openModalLazy } from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react"; import { LazyComponent, useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
@ -161,7 +161,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
infoButton={ infoButton={
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}> <button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
{plugin.options {plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel /> ? <CogWheel />
: <InfoIcon width="24" height="24" />} : <InfoIcon width="24" height="24" />}
</button> </button>

View File

@ -231,7 +231,7 @@ function ThemesTab() {
ref={fileInputRef} ref={fileInputRef}
onChange={onFileUpload} onChange={onFileUpload}
multiple={true} multiple={true}
filters={[{ extensions: ["*.css"] }]} filters={[{ extensions: ["css"] }]}
/> />
</Button> </Button>
) : ( ) : (

View File

@ -249,4 +249,4 @@ function Updater() {
); );
} }
export default IS_WEB ? null : wrapTab(Updater, "Updater"); export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");

1
src/globals.d.ts vendored
View File

@ -35,6 +35,7 @@ declare global {
export var IS_WEB: boolean; export var IS_WEB: boolean;
export var IS_DEV: boolean; export var IS_DEV: boolean;
export var IS_STANDALONE: boolean; export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean;
export var IS_DISCORD_DESKTOP: boolean; export var IS_DISCORD_DESKTOP: boolean;
export var IS_VESKTOP: boolean; export var IS_VESKTOP: boolean;
export var VERSION: string; export var VERSION: string;

View File

@ -22,6 +22,28 @@ import { readFile } from "fs/promises";
import { request } from "https"; import { request } from "https";
import { basename, normalize } from "path"; import { basename, normalize } from "path";
import { getSettings } from "./ipcMain";
// 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;
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 // #region OpenInApp
// These links don't support CORS, so this has to be native // These links don't support CORS, so this has to be native
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/; const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;

View File

@ -20,18 +20,6 @@ import { app } from "electron";
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs"; import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
import { basename, dirname, join } from "path"; import { basename, dirname, join } from "path";
const { setAppUserModelId } = app;
// Apparently requiring Discords updater too early leads into issues,
// copied this workaround from powerCord
app.setAppUserModelId = function (id: string) {
app.setAppUserModelId = setAppUserModelId;
setAppUserModelId.call(this, id);
patchUpdater();
};
function isNewer($new: string, old: string) { function isNewer($new: string, old: string) {
const newParts = $new.slice(4).split(".").map(Number); const newParts = $new.slice(4).split(".").map(Number);
const oldParts = old.slice(4).split(".").map(Number); const oldParts = old.slice(4).split(".").map(Number);
@ -77,25 +65,6 @@ function patchLatest() {
} }
} }
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we // Try to patch latest on before-quit
// need to reinject // Discord's Win32 updater will call app.quit() on restart and open new version on will-quit
function patchUpdater() { app.on("before-quit", patchLatest);
// Array of autoStart paths to try
const autoStartPaths = [
join(require.main!.filename, "..", "autoStart", "win32.js"), // Vanilla
join(require.main!.filename, "..", "autoStart.js") // OpenAsar
];
for (const path of autoStartPaths) {
try {
const { update } = require(path);
require.cache[path]!.exports.update = function () {
update.apply(this, arguments);
patchLatest();
};
} catch {
// Ignore as non-critical
}
}
}

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import(IS_STANDALONE ? "./http" : "./git"); if (!IS_UPDATER_DISABLED)
import(IS_STANDALONE ? "./http" : "./git");

View File

@ -103,7 +103,7 @@ export default definePlugin({
element: require("@components/VencordSettings/ThemesTab").default, element: require("@components/VencordSettings/ThemesTab").default,
className: "vc-themes" className: "vc-themes"
}, },
!IS_WEB && { !IS_UPDATER_DISABLED && {
section: "VencordUpdater", section: "VencordUpdater",
label: "Updater", label: "Updater",
element: require("@components/VencordSettings/UpdaterTab").default, element: require("@components/VencordSettings/UpdaterTab").default,

View File

@ -127,6 +127,7 @@ export const defaultRules = [
"redircnt@yandex.*", "redircnt@yandex.*",
"feature@youtube.com", "feature@youtube.com",
"kw@youtube.com", "kw@youtube.com",
"si@youtu.be",
"wt_zmc", "wt_zmc",
"utm_source", "utm_source",
"utm_content", "utm_content",

View File

@ -0,0 +1,55 @@
/*
* 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 { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { LinkIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Clipboard, Menu } from "@webpack/common";
import type { Channel, User } from "discord-types/general";
interface UserContextProps {
channel: Channel;
guildId?: string;
user: User;
}
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
children.push(
<Menu.MenuItem
id="vc-copy-user-url"
label="Copy User URL"
action={() => Clipboard.copy(`<https://discord.com/users/${user.id}>`)}
icon={LinkIcon}
/>
);
};
export default definePlugin({
name: "CopyUserURLs",
authors: [Devs.castdrian],
description: "Adds a 'Copy User URL' option to the user context menu.",
start() {
addContextMenuPatch("user-context", UserContextMenuPatch);
},
stop() {
removeContextMenuPatch("user-context", UserContextMenuPatch);
},
});

View File

@ -23,10 +23,12 @@ import { Logger } from "@utils/Logger";
import { closeAllModals } from "@utils/modal"; import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater"; import { maybePromptToUpdate } from "@utils/updater";
import { findByPropsLazy } from "@webpack";
import { FluxDispatcher, NavigationRouter } from "@webpack/common"; import { FluxDispatcher, NavigationRouter } from "@webpack/common";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
const CrashHandlerLogger = new Logger("CrashHandler"); const CrashHandlerLogger = new Logger("CrashHandler");
const ModalStack = findByPropsLazy("pushLazy", "popAll");
const settings = definePluginSettings({ const settings = definePluginSettings({
attemptToPreventCrashes: { attemptToPreventCrashes: {
@ -51,8 +53,6 @@ export default definePlugin({
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
enabledByDefault: true, enabledByDefault: true,
popAllModals: undefined as (() => void) | undefined,
settings, settings,
patches: [ patches: [
@ -62,13 +62,6 @@ export default definePlugin({
match: /(?=this\.setState\()/, match: /(?=this\.setState\()/,
replace: "$self.handleCrash(this)||" replace: "$self.handleCrash(this)||"
} }
},
{
find: 'dispatch({type:"MODAL_POP_ALL"})',
replacement: {
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
}
} }
], ],
@ -128,7 +121,7 @@ export default definePlugin({
CrashHandlerLogger.debug("Failed to close open context menu.", err); CrashHandlerLogger.debug("Failed to close open context menu.", err);
} }
try { try {
this.popAllModals?.(); ModalStack?.popAll();
} catch (err) { } catch (err) {
CrashHandlerLogger.debug("Failed to close old modals.", err); CrashHandlerLogger.debug("Failed to close old modals.", err);
} }

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards"; import { isTruthy } from "@utils/guards";
@ -86,8 +86,7 @@ const settings = definePluginSettings({
appID: { appID: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Application ID (required)", description: "Application ID (required)",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: (value: string) => { isValid: (value: string) => {
if (!value) return "Application ID is required."; if (!value) return "Application ID is required.";
if (value && !/^\d+$/.test(value)) return "Application ID must be a number."; if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
@ -97,8 +96,7 @@ const settings = definePluginSettings({
appName: { appName: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Application name (required)", description: "Application name (required)",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: (value: string) => { isValid: (value: string) => {
if (!value) return "Application name is required."; if (!value) return "Application name is required.";
if (value.length > 128) return "Application name must be not longer than 128 characters."; if (value.length > 128) return "Application name must be not longer than 128 characters.";
@ -108,8 +106,7 @@ const settings = definePluginSettings({
details: { details: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Details (line 1)", description: "Details (line 1)",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: (value: string) => { isValid: (value: string) => {
if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters."; if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters.";
return true; return true;
@ -118,8 +115,7 @@ const settings = definePluginSettings({
state: { state: {
type: OptionType.STRING, type: OptionType.STRING,
description: "State (line 2)", description: "State (line 2)",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: (value: string) => { isValid: (value: string) => {
if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters."; if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters.";
return true; return true;
@ -128,8 +124,7 @@ const settings = definePluginSettings({
type: { type: {
type: OptionType.SELECT, type: OptionType.SELECT,
description: "Activity type", description: "Activity type",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
options: [ options: [
{ {
label: "Playing", label: "Playing",
@ -157,16 +152,14 @@ const settings = definePluginSettings({
streamLink: { streamLink: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Twitch.tv or Youtube.com link (only for Streaming activity type)", description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",
restartNeeded: true, onChange: onChange,
onChange: setRpc, disabled: isStreamLinkDisabled,
isDisabled: isStreamLinkDisabled,
isValid: isStreamLinkValid isValid: isStreamLinkValid
}, },
timestampMode: { timestampMode: {
type: OptionType.SELECT, type: OptionType.SELECT,
description: "Timestamp mode", description: "Timestamp mode",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
options: [ options: [
{ {
label: "None", label: "None",
@ -190,9 +183,8 @@ const settings = definePluginSettings({
startTime: { startTime: {
type: OptionType.NUMBER, type: OptionType.NUMBER,
description: "Start timestamp (only for custom timestamp mode)", description: "Start timestamp (only for custom timestamp mode)",
restartNeeded: true, onChange: onChange,
onChange: setRpc, disabled: isTimestampDisabled,
isDisabled: isTimestampDisabled,
isValid: (value: number) => { isValid: (value: number) => {
if (value && value < 0) return "Start timestamp must be greater than 0."; if (value && value < 0) return "Start timestamp must be greater than 0.";
return true; return true;
@ -201,9 +193,8 @@ const settings = definePluginSettings({
endTime: { endTime: {
type: OptionType.NUMBER, type: OptionType.NUMBER,
description: "End timestamp (only for custom timestamp mode)", description: "End timestamp (only for custom timestamp mode)",
restartNeeded: true, onChange: onChange,
onChange: setRpc, disabled: isTimestampDisabled,
isDisabled: isTimestampDisabled,
isValid: (value: number) => { isValid: (value: number) => {
if (value && value < 0) return "End timestamp must be greater than 0."; if (value && value < 0) return "End timestamp must be greater than 0.";
return true; return true;
@ -211,16 +202,14 @@ const settings = definePluginSettings({
}, },
imageBig: { imageBig: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Big image key", description: "Big image key/link",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: isImageKeyValid isValid: isImageKeyValid
}, },
imageBigTooltip: { imageBigTooltip: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Big image tooltip", description: "Big image tooltip",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: (value: string) => { isValid: (value: string) => {
if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters."; if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters.";
return true; return true;
@ -228,16 +217,14 @@ const settings = definePluginSettings({
}, },
imageSmall: { imageSmall: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Small image key", description: "Small image key/link",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: isImageKeyValid isValid: isImageKeyValid
}, },
imageSmallTooltip: { imageSmallTooltip: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Small image tooltip", description: "Small image tooltip",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: (value: string) => { isValid: (value: string) => {
if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters."; if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters.";
return true; return true;
@ -246,8 +233,7 @@ const settings = definePluginSettings({
buttonOneText: { buttonOneText: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Button 1 text", description: "Button 1 text",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: (value: string) => { isValid: (value: string) => {
if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters."; if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters.";
return true; return true;
@ -256,14 +242,12 @@ const settings = definePluginSettings({
buttonOneURL: { buttonOneURL: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Button 1 URL", description: "Button 1 URL",
restartNeeded: true, onChange: onChange
onChange: setRpc
}, },
buttonTwoText: { buttonTwoText: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Button 2 text", description: "Button 2 text",
restartNeeded: true, onChange: onChange,
onChange: setRpc,
isValid: (value: string) => { isValid: (value: string) => {
if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters."; if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters.";
return true; return true;
@ -272,26 +256,29 @@ const settings = definePluginSettings({
buttonTwoURL: { buttonTwoURL: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Button 2 URL", description: "Button 2 URL",
restartNeeded: true, onChange: onChange
onChange: setRpc
} }
}); });
function isStreamLinkDisabled(): boolean { function onChange() {
setRpc(true);
if (Settings.plugins.CustomRPC.enabled) setRpc();
}
function isStreamLinkDisabled() {
return settings.store.type !== ActivityType.STREAMING; return settings.store.type !== ActivityType.STREAMING;
} }
function isStreamLinkValid(): boolean | string { function isStreamLinkValid(value: string) {
if (settings.store.type === ActivityType.STREAMING && settings.store.streamLink && !/(https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+)/.test(settings.store.streamLink)) return "Streaming link must be a valid URL."; if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
return true; return true;
} }
function isTimestampDisabled(): boolean { function isTimestampDisabled() {
return settings.store.timestampMode !== TimestampMode.CUSTOM; return settings.store.timestampMode !== TimestampMode.CUSTOM;
} }
function isImageKeyValid(value: string) { function isImageKeyValid(value: string) {
if (!/https?:\/\//.test(value)) return true;
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)"; if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)"; if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
return true; return true;
@ -342,13 +329,10 @@ async function createActivity(): Promise<Activity | undefined> {
}; };
break; break;
case TimestampMode.CUSTOM: case TimestampMode.CUSTOM:
if (startTime) { if (startTime || endTime) {
activity.timestamps = { activity.timestamps = {};
start: startTime, if (startTime) activity.timestamps.start = startTime;
}; if (endTime) activity.timestamps.end = endTime;
if (endTime) {
activity.timestamps.end = endTime;
}
} }
break; break;
case TimestampMode.NONE: case TimestampMode.NONE:

View File

@ -0,0 +1,26 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
// The entire code of this plugin can be found in ipcPlugins
export default definePlugin({
name: "FixSpotifyEmbeds",
description: "Fixes spotify embeds being incredibly loud by letting you customise the volume",
authors: [Devs.Ven],
settings: definePluginSettings({
volume: {
type: OptionType.SLIDER,
description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud",
markers: makeRange(0, 100, 10),
stickToMarkers: false,
default: 10
}
})
});

View File

@ -32,12 +32,18 @@ function makeIcon(showCurrentGame?: boolean) {
return function () { return function () {
return ( return (
<svg <svg
width="24" width="20"
height="24" height="20"
viewBox="0 96 960 960" viewBox="0 0 24 24"
> >
<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" /> <path fill="currentColor" mask="url(#gameActivityMask)" d="M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z" />
{!showCurrentGame && <line x1="920" y1="280" x2="40" y2="880" stroke="var(--status-danger)" stroke-width="80" />} {!showCurrentGame && <>
<mask id="gameActivityMask" >
<rect fill="white" x="0" y="0" width="24" height="24" />
<path fill="black" d="M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z"/>
</mask>
<path fill="var(--status-danger)" d="M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z" />
</>}
</svg> </svg>
); );
}; };
@ -60,7 +66,7 @@ function GameActivityToggleButton() {
export default definePlugin({ export default definePlugin({
name: "GameActivityToggle", name: "GameActivityToggle",
description: "Adds a button next to the mic and deafen button to toggle game activity.", description: "Adds a button next to the mic and deafen button to toggle game activity.",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz, Devs.RuukuLada],
dependencies: ["SettingsStoreAPI"], dependencies: ["SettingsStoreAPI"],
patches: [ patches: [

View File

@ -49,7 +49,7 @@ export default definePlugin({
await this.buildCss(); await this.buildCss();
addButton("HideAttachments", msg => { addButton("HideAttachments", msg => {
if (!msg.attachments.length && !msg.embeds.length) return null; if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
const isHidden = hiddenMessages.has(msg.id); const isHidden = hiddenMessages.has(msg.id);
@ -72,7 +72,7 @@ export default definePlugin({
async buildCss() { async buildCss() {
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(","); const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
style.textContent = ` style.textContent = `
:is(${elements}) [class*="embedWrapper"] { :is(${elements}) :is([class*="embedWrapper", [class*"clickableSticker"]) {
/* important is not necessary, but add it to make sure bad themes won't break it */ /* important is not necessary, but add it to make sure bad themes won't break it */
display: none !important; display: none !important;
} }

View File

@ -30,6 +30,9 @@ const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; }; getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
}; };
const sharedIntlNumberFormat = new Intl.NumberFormat();
const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
function MemberCount() { function MemberCount() {
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const { groups } = useStateFromStores( const { groups } = useStateFromStores(
@ -57,7 +60,7 @@ function MemberCount() {
alignContent: "center", alignContent: "center",
gap: 0 gap: 0
}}> }}>
<Tooltip text={`${online} Online in this Channel`} position="bottom"> <Tooltip text={`${numberFormat(online)} online in this channel`} position="bottom">
{props => ( {props => (
<div {...props}> <div {...props}>
<span <span
@ -70,11 +73,11 @@ function MemberCount() {
marginRight: "0.5em" marginRight: "0.5em"
}} }}
/> />
<span style={{ color: "var(--green-360)" }}>{online}</span> <span style={{ color: "var(--green-360)" }}>{numberFormat(online)}</span>
</div> </div>
)} )}
</Tooltip> </Tooltip>
<Tooltip text={`${total} Total Server Members`} position="bottom"> <Tooltip text={`${numberFormat(total)} total server members`} position="bottom">
{props => ( {props => (
<div {...props}> <div {...props}>
<span <span
@ -88,7 +91,7 @@ function MemberCount() {
marginLeft: "1em" marginLeft: "1em"
}} }}
/> />
<span style={{ color: "var(--primary-400)" }}>{total}</span> <span style={{ color: "var(--primary-400)" }}>{numberFormat(total)}</span>
</div> </div>
)} )}
</Tooltip> </Tooltip>

View File

@ -18,6 +18,7 @@
import { addAccessory } from "@api/MessageAccessories"; import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStore";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js"; import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
@ -318,9 +319,10 @@ function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbe
/>; />;
} }
const compactModeEnabled = getSettingStoreLazy<boolean>("textAndImages", "messageDisplayCompact")!;
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
const { message, channel, guildID } = props; const { message, channel, guildID } = props;
const isDM = guildID === "@me"; const isDM = guildID === "@me";
const images = getImages(message); const images = getImages(message);
const { parse } = Parser; const { parse } = Parser;
@ -336,7 +338,7 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span> <span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
</Text> </Text>
} }
compact={false} compact={compactModeEnabled.getSetting()}
content={ content={
<> <>
{message.content || message.attachments.length <= images.length {message.content || message.attachments.length <= images.length
@ -363,7 +365,7 @@ export default definePlugin({
name: "MessageLinkEmbeds", name: "MessageLinkEmbeds",
description: "Adds a preview to messages that link another message", description: "Adds a preview to messages that link another message",
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev], authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
dependencies: ["MessageAccessoriesAPI"], dependencies: ["MessageAccessoriesAPI", "SettingsStoreAPI"],
patches: [ patches: [
{ {
find: ".embedCard", find: ".embedCard",

View File

@ -169,21 +169,14 @@ export default definePlugin({
try { try {
if (cache == null || (!isBulk && !cache.has(data.id))) return cache; if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger; const mutate = (id: string) => {
const myId = UserStore.getCurrentUser().id;
function mutate(id: string) {
const msg = cache.get(id); const msg = cache.get(id);
if (!msg) return; if (!msg) return;
const EPHEMERAL = 64; const EPHEMERAL = 64;
const shouldIgnore = data.mlDeleted || const shouldIgnore = data.mlDeleted ||
(msg.flags & EPHEMERAL) === EPHEMERAL || (msg.flags & EPHEMERAL) === EPHEMERAL ||
ignoreBots && msg.author?.bot || this.shouldIgnore(msg);
ignoreSelf && msg.author?.id === myId ||
ignoreUsers.includes(msg.author?.id) ||
ignoreChannels.includes(msg.channel_id) ||
ignoreGuilds.includes(ChannelStore.getChannel(msg.channel_id)?.guild_id);
if (shouldIgnore) { if (shouldIgnore) {
cache = cache.remove(id); cache = cache.remove(id);
@ -192,7 +185,7 @@ export default definePlugin({
.set("deleted", true) .set("deleted", true)
.set("attachments", m.attachments.map(a => (a.deleted = true, a)))); .set("attachments", m.attachments.map(a => (a.deleted = true, a))));
} }
} };
if (isBulk) { if (isBulk) {
data.ids.forEach(mutate); data.ids.forEach(mutate);
@ -205,6 +198,17 @@ export default definePlugin({
return cache; return cache;
}, },
shouldIgnore(message: any) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
return ignoreBots && message.author?.bot ||
ignoreSelf && message.author?.id === myId ||
ignoreUsers.includes(message.author?.id) ||
ignoreChannels.includes(message.channel_id) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
},
// Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996 // Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996
patches: [ patches: [
{ {
@ -237,7 +241,7 @@ export default definePlugin({
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/, match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
replace: "$1" + replace: "$1" +
".update($3,m =>" + ".update($3,m =>" +
" (($2.message.flags & 64) === 64 || (Vencord.Settings.plugins.MessageLogger.ignoreBots && $2.message.author?.bot) || (Vencord.Settings.plugins.MessageLogger.ignoreSelf && $2.message.author?.id === Vencord.Webpack.Common.UserStore.getCurrentUser().id)) ? m :" + " (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" +
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" + " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" + " m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" + " m" +

View File

@ -54,6 +54,8 @@ interface IVoiceChannelEffectSendEvent {
const MOYAI = "🗿"; const MOYAI = "🗿";
const MOYAI_URL = const MOYAI_URL =
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3"; "https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
const MOYAI_URL_HD =
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai_hd.wav";
const settings = definePluginSettings({ const settings = definePluginSettings({
volume: { volume: {
@ -63,6 +65,14 @@ const settings = definePluginSettings({
default: 0.5, default: 0.5,
stickToMarkers: false stickToMarkers: false
}, },
quality: {
description: "Quality of the 🗿🗿🗿",
type: OptionType.SELECT,
options: [
{ label: "Normal", value: "Normal", default: true },
{ label: "HD", value: "HD" }
],
},
triggerWhenUnfocused: { triggerWhenUnfocused: {
description: "Trigger the 🗿 even when the window is unfocused", description: "Trigger the 🗿 even when the window is unfocused",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -157,7 +167,11 @@ function getMoyaiCount(message: string) {
function boom() { function boom() {
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return; if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
const audioElement = document.createElement("audio"); const audioElement = document.createElement("audio");
audioElement.src = MOYAI_URL;
audioElement.src = settings.store.quality === "HD"
? MOYAI_URL_HD
: MOYAI_URL;
audioElement.volume = settings.store.volume; audioElement.volume = settings.store.volume;
audioElement.play(); audioElement.play();
} }

View File

@ -55,8 +55,8 @@ export default definePlugin({
{ {
find: '"MaskedLinkStore"', find: '"MaskedLinkStore"',
replacement: { replacement: {
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,200}\.trusted)/, match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,250}\.trusted)/,
replace: "return $self.handleLink(...arguments).then(handled => handled || $1)" replace: "return $self.handleLink(...arguments).then(handled => handled||$1)"
} }
}, },
// Make Spotify profile activity links open in app on web // Make Spotify profile activity links open in app on web

View File

@ -178,12 +178,12 @@ export default definePlugin({
start() { start() {
addContextMenuPatch("user-context", this.userContextMenuPatch); addContextMenuPatch("user-context", this.userContextMenuPatch);
addContextMenuPatch("channel-context", this.channelContextMenuPatch); addContextMenuPatch("channel-context", this.channelContextMenuPatch);
addContextMenuPatch("guild-context", this.guildContextMenuPatch); addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
}, },
stop() { stop() {
removeContextMenuPatch("user-context", this.userContextMenuPatch); removeContextMenuPatch("user-context", this.userContextMenuPatch);
removeContextMenuPatch("channel-context", this.channelContextMenuPatch); removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
removeContextMenuPatch("guild-context", this.guildContextMenuPatch); removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
}, },
}); });

View File

@ -122,6 +122,14 @@ export default definePlugin({
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c))) // ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))" replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
} }
},
// fix alt+shift+up/down
{
find: '"alt+shift+down"',
replacement: {
match: /(?<=return \i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
} }
},
] ]
}); });

View File

@ -20,7 +20,7 @@ import { sendBotMessage } from "@api/Commands";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common"; import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
interface Props { interface Props {
type: { type: {
@ -31,10 +31,9 @@ interface Props {
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
export function PreviewButton(chatBoxProps: Props) { export function PreviewButton(chatBoxProps: Props) {
if (chatBoxProps.type.analyticsName !== "normal") return null;
const channelId = SelectedChannelStore.getChannelId(); const channelId = SelectedChannelStore.getChannelId();
const draft = getDraft(channelId); const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
if (chatBoxProps.type.analyticsName !== "normal") return null;
if (!draft) return null; if (!draft) return null;
return ( return (

View File

@ -0,0 +1,247 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { classNameFactory } from "@api/Styles";
import { openImageModal, openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react";
import { findByCode, findByPropsLazy } from "@webpack";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
import { Guild, User } from "discord-types/general";
const IconUtils = findByPropsLazy("getGuildBannerURL");
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
const UserRow = LazyComponent(() => findByCode(".listDiscriminator"));
const cl = classNameFactory("vc-gp-");
export function openGuildProfileModal(guild: Guild) {
openModal(props =>
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<GuildProfileModal guild={guild} />
</ModalRoot>
);
}
const enum Tabs {
ServerInfo,
Friends,
BlockedUsers
}
interface GuildProps {
guild: Guild;
}
interface RelationshipProps extends GuildProps {
setCount(count: number): void;
}
const fetched = {
friends: false,
blocked: false
};
function renderTimestamp(timestamp: number) {
return (
<Timestamp timestamp={moment(timestamp)} />
);
}
function GuildProfileModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState<number>();
const [blockedCount, setBlockedCount] = useState<number>();
useEffect(() => {
fetched.friends = false;
fetched.blocked = false;
}, []);
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
const bannerUrl = guild.banner && IconUtils.getGuildBannerURL({
id: guild.id,
banner: guild.banner
}, true).replace(/\?size=\d+$/, "?size=1024");
const iconUrl = guild.icon && IconUtils.getGuildIconURL({
id: guild.id,
icon: guild.icon,
canAnimate: true,
size: 512
});
return (
<div className={cl("root")}>
{bannerUrl && currentTab === Tabs.ServerInfo && (
<img
className={cl("banner")}
src={bannerUrl}
alt=""
onClick={() => openImageModal(bannerUrl)}
/>
)}
<div className={cl("header")}>
{guild.icon
? <img
src={iconUrl}
alt=""
onClick={() => openImageModal(iconUrl)}
/>
: <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{guild.acronym}</div>
}
<div className={cl("name-and-description")}>
<Forms.FormTitle tag="h5" className={cl("name")}>{guild.name}</Forms.FormTitle>
{guild.description && <Forms.FormText>{guild.description}</Forms.FormText>}
</div>
</div>
<TabBar
type="top"
look="brand"
className={cl("tab-bar")}
selectedItem={currentTab}
onItemSelect={setCurrentTab}
>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.ServerInfo })}
id={Tabs.ServerInfo}
>
Server Info
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.Friends })}
id={Tabs.Friends}
>
Friends{friendCount !== undefined ? ` (${friendCount})` : ""}
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.BlockedUsers })}
id={Tabs.BlockedUsers}
>
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
</TabBar.Item>
</TabBar>
<div className={cl("tab-content")}>
{currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />}
{currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
</div>
</div>
);
}
function Owner(guildId: string, owner: User) {
const guildAvatar = GuildMemberStore.getMember(guildId, owner.id)?.avatar;
const ownerAvatarUrl =
guildAvatar
? IconUtils.getGuildMemberAvatarURLSimple({
userId: owner!.id,
avatar: guildAvatar,
guildId,
canAnimate: true
}, true)
: IconUtils.getUserAvatarURL(owner, true);
return (
<div className={cl("owner")}>
<img src={ownerAvatarUrl} alt="" onClick={() => openImageModal(ownerAvatarUrl)} />
{Parser.parse(`<@${owner.id}>`)}
</div>
);
}
function ServerInfoTab({ guild }: GuildProps) {
const [owner] = useAwaiter(() => UserUtils.fetchUser(guild.ownerId), {
deps: [guild.ownerId],
fallbackValue: null
});
const Fields = {
"Server Owner": owner ? Owner(guild.id, owner) : "Loading...",
"Created At": renderTimestamp(SnowflakeUtils.extractTimestamp(guild.id)),
"Joined At": renderTimestamp(guild.joinedAt.getTime()),
"Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload
"Preferred Locale": guild.preferredLocale || "-",
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
"Roles": Object.keys(guild.roles).length - 1, // - @everyone
};
return (
<div className={cl("info")}>
{Object.entries(Fields).map(([name, node]) =>
<div className={cl("server-info-pair")} key={name}>
<Forms.FormTitle tag="h5">{name}</Forms.FormTitle>
{typeof node === "string" ? <span>{node}</span> : node}
</div>
)}
</div>
);
}
function FriendsTab({ guild, setCount }: RelationshipProps) {
return UserList("friends", guild, RelationshipStore.getFriendIDs(), setCount);
}
function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
const blockedIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isBlocked(id));
return UserList("blocked", guild, blockedIds, setCount);
}
function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) {
const missing = [] as string[];
const members = [] as string[];
for (const id of ids) {
if (GuildMemberStore.isMember(guild.id, id))
members.push(id);
else
missing.push(id);
}
// Used for side effects (rerender on member request success)
useStateFromStores(
[GuildMemberStore],
() => GuildMemberStore.getMemberIds(guild.id),
null,
(old, curr) => old.length === curr.length
);
useEffect(() => {
if (!fetched[type] && missing.length) {
fetched[type] = true;
FluxDispatcher.dispatch({
type: "GUILD_MEMBERS_REQUEST",
guildIds: [guild.id],
userIds: missing
});
}
}, []);
useEffect(() => setCount(members.length), [members.length]);
return (
<ScrollerThin fade className={cl("scroller")}>
{members.map(id =>
<UserRow
user={UserStore.getUser(id)}
status={PresenceStore.getStatus(id) || "offline"}
onSelect={() => openUserProfile(id)}
onContextMenu={() => { }}
/>
)}
</ScrollerThin>
);
}

View File

@ -0,0 +1,7 @@
# ServerProfile
Allows you to view info about servers and see friends and blocked users
![image](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)
![image](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)
![image](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)

View File

@ -0,0 +1,40 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
import { Guild } from "discord-types/general";
import { openGuildProfileModal } from "./GuildProfileModal";
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => {
const group = findGroupChildrenByChildId("privacy", children);
group?.push(
<Menu.MenuItem
id="vc-server-profile"
label="Server Profile"
action={() => openGuildProfileModal(guild)}
/>
);
};
export default definePlugin({
name: "ServerProfile",
description: "Allows you to view info about a server by right clicking it in the server list",
authors: [Devs.Ven, Devs.Nuckyz],
tags: ["guild", "info"],
start() {
addContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
},
stop() {
removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
}
});

View File

@ -0,0 +1,97 @@
.vc-gp-root {
height: 100%;
user-select: text;
}
.vc-gp-banner {
width: 100%;
cursor: pointer;
}
.vc-gp-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5em;
margin: 0.5em;
}
.vc-gp-header img {
width: 48px;
height: 48px;
cursor: pointer;
}
.vc-gp-name-and-description {
display: flex;
flex-direction: column;
gap: 0.2em;
}
.vc-gp-name {
margin: 0;
}
.vc-gp-tab-bar {
border-bottom: 2px solid var(--background-modifier-accent);
margin: 20px 12px 0;
display: flex;
gap: 40px;
align-items: stretch;
flex-direction: row;
}
.vc-gp-tab {
border-bottom: 2px solid transparent;
color: var(--interactive-normal);
cursor: pointer;
height: 39px;
line-height: 14px;
}
.vc-gp-tab-content {
margin: 1em;
}
.vc-gp-tab:where(.vc-gp-selected, :hover, :focus) {
border-bottom-color: var(--interactive-active);
}
.vc-gp-info {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1em;
}
.vc-gp-server-info-pair {
color: var(--text-normal);
}
.vc-gp-server-info-pair [class^="timestamp"] {
margin-left: 0;
}
.vc-gp-owner {
display: flex;
align-items: center;
gap: 0.2em;
}
.vc-gp-owner img {
height: 20px;
border-radius: 50%;
cursor: pointer;
}
.vc-gp-scroller {
width: 100%;
max-height: 500px;
}
.vc-gp-scroller [class^="listRow"] {
margin: 1px 0;
}
.vc-gp-scroller [class^="listRow"]:hover {
background-color: var(--background-modifier-hover);
}

View File

@ -21,13 +21,14 @@ import "./styles.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Message } from "discord-types/general"; import { Message, User } from "discord-types/general";
interface UsernameProps { interface UsernameProps {
author: { nick: string; }; author: { nick: string; };
message: Message; message: Message;
withMentionPrefix?: boolean; withMentionPrefix?: boolean;
isRepliedMessage: boolean; isRepliedMessage: boolean;
userOverride?: User;
} }
const settings = definePluginSettings({ const settings = definePluginSettings({
@ -67,11 +68,12 @@ export default definePlugin({
], ],
settings, settings,
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix }: UsernameProps) => { renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
try { try {
let { username } = message.author; const user = userOverride ?? message.author;
let { username } = user;
if (settings.store.displayNames) if (settings.store.displayNames)
username = (message.author as any).globalName || username; username = (user as any).globalName || username;
const { nick } = author; const { nick } = author;
const prefix = withMentionPrefix ? "@" : ""; const prefix = withMentionPrefix ? "@" : "";

View File

@ -0,0 +1,35 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "ShowTimeouts",
description: "Display member timeout icons in chat regardless of permissions.",
authors: [Devs.Dolfies],
patches: [
{
find: "showCommunicationDisabledStyles",
replacement: {
match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/,
replace: "",
},
},
],
});

View File

@ -0,0 +1,20 @@
# ThemeAttributes
This plugin adds data attributes to various elements inside Discord
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
## Available Attributes
### All Tab Bars (User Settings, Server Settings, etc)
`data-tab-id` contains the id of that tab
![image](https://github.com/Vendicated/Vencord/assets/45497981/1263b782-f673-4f09-820c-4cc366d062ad)
### Chat Messages
- `data-author-id` contains the id of the author
- `data-is-self` is a boolean indicating whether this is the current user's message
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)

View File

@ -0,0 +1,45 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
export default definePlugin({
name: "ThemeAttributes",
description: "Adds data attributes to various elements for theming purposes",
authors: [Devs.Ven],
patches: [
// Add data-tab-id to all tab bar items
// This for examples applies to the User and Server settings sidebars
{
find: ".tabBarRef",
replacement: {
match: /style:this\.getStyle\(\),role:"tab"/,
replace: "$&,'data-tab-id':this.props.id"
}
},
// Add data-author-id and data-is-self to all messages
{
find: ".messageListItem",
replacement: {
match: /\.messageListItem(?=,"aria)/,
replace: "$&,...$self.getMessageProps(arguments[0])"
}
}
],
getMessageProps(props: { message: Message; }) {
const authorId = props.message?.author?.id;
return {
"data-author-id": authorId,
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id
};
}
});

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover"; import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -25,9 +26,11 @@ import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc"; import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Parser, Text } from "@webpack/common"; import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Forms, Menu, Parser, Text } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
const CodeContainerClasses = findByPropsLazy("markup", "codeContainer");
const CopyIcon = () => { const CopyIcon = () => {
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="22" height="22"> return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="22" height="22">
@ -60,17 +63,13 @@ function cleanMessage(msg: Message) {
function CodeBlock(props: { content: string, lang: string; }) { function CodeBlock(props: { content: string, lang: string; }) {
return ( return (
// make text selectable <div className={CodeContainerClasses.markup}>
<div style={{ userSelect: "text" }}>
{Parser.defaultRules.codeBlock.react(props, null, {})} {Parser.defaultRules.codeBlock.react(props, null, {})}
</div> </div>
); );
} }
function openViewRawModal(msg: Message) { function openViewRawModal(json: string, type: string, msgContent?: string) {
msg = cleanMessage(msg);
const msgJson = JSON.stringify(msg, null, 4);
const key = openModal(props => ( const key = openModal(props => (
<ErrorBoundary> <ErrorBoundary>
<ModalRoot {...props} size={ModalSize.LARGE}> <ModalRoot {...props} size={ModalSize.LARGE}>
@ -80,26 +79,28 @@ function openViewRawModal(msg: Message) {
</ModalHeader> </ModalHeader>
<ModalContent> <ModalContent>
<div style={{ padding: "16px 0" }}> <div style={{ padding: "16px 0" }}>
{!!msg.content && ( {!!msgContent && (
<> <>
<Forms.FormTitle tag="h5">Content</Forms.FormTitle> <Forms.FormTitle tag="h5">Content</Forms.FormTitle>
<CodeBlock content={msg.content} lang="" /> <CodeBlock content={msgContent} lang="" />
<Forms.FormDivider className={Margins.bottom20} /> <Forms.FormDivider className={Margins.bottom20} />
</> </>
)} )}
<Forms.FormTitle tag="h5">Message Data</Forms.FormTitle> <Forms.FormTitle tag="h5">{type} Data</Forms.FormTitle>
<CodeBlock content={msgJson} lang="json" /> <CodeBlock content={json} lang="json" />
</div> </div>
</ModalContent > </ModalContent >
<ModalFooter> <ModalFooter>
<Flex cellSpacing={10}> <Flex cellSpacing={10}>
<Button onClick={() => copyWithToast(msgJson, "Message data copied to clipboard!")}> <Button onClick={() => copyWithToast(json, `${type} data copied to clipboard!`)}>
Copy Message JSON Copy {type} JSON
</Button> </Button>
<Button onClick={() => copyWithToast(msg.content, "Content copied to clipboard!")}> {!!msgContent && (
<Button onClick={() => copyWithToast(msgContent, "Content copied to clipboard!")}>
Copy Raw Content Copy Raw Content
</Button> </Button>
)}
</Flex> </Flex>
</ModalFooter> </ModalFooter>
</ModalRoot > </ModalRoot >
@ -107,6 +108,13 @@ function openViewRawModal(msg: Message) {
)); ));
} }
function openViewRawModalMessage(msg: Message) {
msg = cleanMessage(msg);
const msgJson = JSON.stringify(msg, null, 4);
return openViewRawModal(msgJson, "Message", msg.content);
}
const settings = definePluginSettings({ const settings = definePluginSettings({
clickMethod: { clickMethod: {
description: "Change the button to view the raw content/data of any message.", description: "Change the button to view the raw content/data of any message.",
@ -118,10 +126,33 @@ const settings = definePluginSettings({
} }
}); });
function MakeContextCallback(name: string) {
const callback: NavContextMenuPatchCallback = (children, props) => () => {
const lastChild = children.at(-1);
if (lastChild?.key === "developer-actions") {
const p = lastChild.props;
if (!Array.isArray(p.children))
p.children = [p.children];
({ children } = p);
}
children.splice(-1, 0,
<Menu.MenuItem
id={`vc-view-${name.toLowerCase()}-raw`}
label="View Raw"
action={() => openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)}
icon={CopyIcon}
/>
);
};
return callback;
}
export default definePlugin({ export default definePlugin({
name: "ViewRaw", name: "ViewRaw",
description: "Copy and view the raw content/data of any message.", description: "Copy and view the raw content/data of any message, channel or guild",
authors: [Devs.KingFish, Devs.Ven, Devs.rad], authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
dependencies: ["MessagePopoverAPI"], dependencies: ["MessagePopoverAPI"],
settings, settings,
@ -131,7 +162,7 @@ export default definePlugin({
if (settings.store.clickMethod === "Right") { if (settings.store.clickMethod === "Right") {
copyWithToast(msg.content); copyWithToast(msg.content);
} else { } else {
openViewRawModal(msg); openViewRawModalMessage(msg);
} }
}; };
@ -143,7 +174,7 @@ export default definePlugin({
} else { } else {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
openViewRawModal(msg); openViewRawModalMessage(msg);
} }
}; };
@ -160,9 +191,16 @@ export default definePlugin({
onContextMenu: handleContextMenu onContextMenu: handleContextMenu
}; };
}); });
addContextMenuPatch("guild-context", MakeContextCallback("Guild"));
addContextMenuPatch("channel-context", MakeContextCallback("Channel"));
addContextMenuPatch("user-context", MakeContextCallback("User"));
}, },
stop() { stop() {
removeButton("CopyRawMessage"); removeButton("CopyRawMessage");
removeContextMenuPatch("guild-context", MakeContextCallback("Guild"));
removeContextMenuPatch("channel-context", MakeContextCallback("Channel"));
removeContextMenuPatch("user-context", MakeContextCallback("User"));
} }
}); });

View File

@ -343,6 +343,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Rini", name: "Rini",
id: 1079479184478441643n id: 1079479184478441643n
}, },
castdrian: {
name: "castdrian",
id: 224617799434108928n
},
Arrow: { Arrow: {
name: "arrow", name: "arrow",
id: 958158495302176778n id: 958158495302176778n
@ -351,6 +355,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "bb010g", name: "bb010g",
id: 72791153467990016n, id: 72791153467990016n,
}, },
Dolfies: {
name: "Dolfies",
id: 852892297661906993n,
},
RuukuLada: {
name: "RuukuLada",
id: 119705748346241027n,
},
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly

View File

@ -74,6 +74,16 @@ export function isObject(obj: unknown): obj is object {
return typeof obj === "object" && obj !== null && !Array.isArray(obj); return typeof obj === "object" && obj !== null && !Array.isArray(obj);
} }
/**
* Check if an object is empty or in other words has no own properties
*/
export function isObjectEmpty(obj: object) {
for (const k in obj)
if (Object.hasOwn(obj, k)) return false;
return true;
}
/** /**
* Returns null if value is not a URL, otherwise return URL object. * Returns null if value is not a URL, otherwise return URL object.
* Avoids having to wrap url checks in a try/catch * Avoids having to wrap url checks in a try/catch

View File

@ -63,7 +63,7 @@ export async function update() {
export const getRepo = () => Unwrap(VencordNative.updater.getRepo()); export const getRepo = () => Unwrap(VencordNative.updater.getRepo());
export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) { export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {
if (IS_WEB) return; if (IS_WEB || IS_UPDATER_DISABLED) return;
if (checkForDev && IS_DEV) return; if (checkForDev && IS_DEV) return;
try { try {