Compare commits

..

31 Commits

Author SHA1 Message Date
V
95b12f109a Add README 2023-09-05 18:29:00 +02:00
Nuckyz
f40ab912eb Fix discord horrors 2023-09-05 01:37:24 -03:00
Nuckyz
700301ffbb Patch guild header popout context too 2023-09-05 01:37:06 -03:00
Nuckyz
d92894697b Improve timestamps and vanity link 2023-09-05 01:36:32 -03:00
V
47569d2ffa cleanup 2023-09-05 06:00:47 +02:00
V
d3b18bbcd2 Add relationship counts 2023-09-05 05:58:02 +02:00
V
eeedc531a3 fix explosive issues 2023-09-05 05:35:10 +02:00
V
59a2b834a6 bleh 2023-09-05 05:34:03 +02:00
V
1cfeaf77c1 add friends & blocked users 2023-09-05 05:18:26 +02:00
V
1757d17661 add channel and role count 2023-09-05 04:23:21 +02:00
V
9485d2457a improve owner 2023-09-05 04:03:31 +02:00
V
3fd2fc1d61 nicer modal 2023-09-05 03:53:33 +02:00
V
49bc6b8fd6 new plugin: server profile 2023-09-04 23:36:48 +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
34 changed files with 686 additions and 140 deletions

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

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

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

@ -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/>.
*/ */
if (!IS_UPDATER_DISABLED)
import(IS_STANDALONE ? "./http" : "./git"); 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

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

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

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

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

@ -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,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "bb010g", name: "bb010g",
id: 72791153467990016n, id: 72791153467990016n,
}, },
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

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