Merge branch 'Vendicated:main' into main

This commit is contained in:
Bruno 2024-11-03 03:28:33 -03:00 committed by GitHub
commit 529fd26aed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
164 changed files with 2435 additions and 1863 deletions

View file

@ -5,6 +5,7 @@
// @author Vendicated (https://github.com/Vendicated)
// @namespace https://github.com/Vendicated/Vencord
// @supportURL https://github.com/Vendicated/Vencord
// @icon https://raw.githubusercontent.com/Vendicated/Vencord/refs/heads/main/browser/icon.png
// @license GPL-3.0
// @match *://*.discord.com/*
// @grant GM_xmlhttpRequest

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.9.7",
"version": "1.10.5",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -70,6 +70,7 @@
"stylelint": "^16.8.1",
"stylelint-config-standard": "^36.0.1",
"ts-patch": "^3.2.1",
"ts-pattern": "^5.3.1",
"tsx": "^4.16.5",
"type-fest": "^4.23.0",
"typescript": "^5.5.4",

8
pnpm-lock.yaml generated
View file

@ -116,6 +116,9 @@ importers:
ts-patch:
specifier: ^3.2.1
version: 3.2.1
ts-pattern:
specifier: ^5.3.1
version: 5.3.1
tsx:
specifier: ^4.16.5
version: 4.16.5
@ -2524,6 +2527,9 @@ packages:
resolution: {integrity: sha512-hlR43v+GUIUy8/ZGFP1DquEqPh7PFKQdDMTAmYt671kCCA6AkDQMoeFaFmZ7ObPLYOmpMgyKUqL1C+coFMf30w==}
hasBin: true
ts-pattern@5.3.1:
resolution: {integrity: sha512-1RUMKa8jYQdNfmnK4jyzBK3/PS/tnjcZ1CW0v1vWDeYe5RBklc/nquw03MEoB66hVBm4BnlCfmOqDVxHyT1DpA==}
tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
@ -5158,6 +5164,8 @@ snapshots:
semver: 7.6.3
strip-ansi: 6.0.1
ts-pattern@5.3.1: {}
tsconfig-paths@3.15.0:
dependencies:
'@types/json5': 0.0.29

View file

@ -21,7 +21,7 @@ import esbuild from "esbuild";
import { readdir } from "fs/promises";
import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, watch } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch } from "./common.mjs";
const defines = {
IS_STANDALONE,
@ -131,7 +131,7 @@ await Promise.all([
sourcemap,
plugins: [
globPlugins("discordDesktop"),
...commonOpts.plugins
...commonRendererPlugins
],
define: {
...defines,
@ -180,7 +180,7 @@ await Promise.all([
sourcemap,
plugins: [
globPlugins("vencordDesktop"),
...commonOpts.plugins
...commonRendererPlugins
],
define: {
...defines,

View file

@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
import { join } from "path";
import Zip from "zip-local";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
@ -36,7 +36,7 @@ const commonOptions = {
external: ["~plugins", "~git-hash", "/assets/*"],
plugins: [
globPlugins("web"),
...commonOpts.plugins,
...commonRendererPlugins
],
target: ["esnext"],
define: {
@ -116,7 +116,12 @@ await Promise.all(
}
})
]
);
).catch(err => {
console.error("Build failed");
console.error(err.message);
if (!commonOpts.watch)
process.exit(1);
});;
/**
* @type {(dir: string) => Promise<string[]>}

View file

@ -28,6 +28,7 @@ import { join, relative } from "path";
import { promisify } from "util";
import { getPluginTarget } from "../utils.mjs";
import { builtinModules } from "module";
/** @type {import("../../package.json")} */
const PackageJSON = JSON.parse(readFileSync("package.json"));
@ -292,6 +293,18 @@ export const stylePlugin = {
}
};
/**
* @type {(filter: RegExp, message: string) => import("esbuild").Plugin}
*/
export const banImportPlugin = (filter, message) => ({
name: "ban-imports",
setup: build => {
build.onResolve({ filter }, () => {
return { errors: [{ text: message }] };
});
}
});
/**
* @type {import("esbuild").BuildOptions}
*/
@ -311,3 +324,16 @@ export const commonOpts = {
// Work around https://github.com/evanw/esbuild/issues/2460
tsconfig: "./scripts/build/tsconfig.esbuild.json"
};
const escapedBuiltinModules = builtinModules
.map(m => m.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
.join("|");
const builtinModuleRegex = new RegExp(`^(node:)?(${escapedBuiltinModules})$`);
export const commonRendererPlugins = [
banImportPlugin(builtinModuleRegex, "Cannot import node inbuilt modules in browser code. You need to use a native.ts file"),
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
...commonOpts.plugins
];

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { makeCodeblock } from "@utils/text";
import { sendBotMessage } from "./commandHelpers";
@ -46,10 +47,10 @@ export let RequiredMessageOption: Option = ReqPlaceholder;
export const _init = function (cmds: Command[]) {
try {
BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
} catch (e) {
console.error("Failed to load CommandsApi");
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
}
return cmds;
} as never;
@ -138,6 +139,8 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
throw new Error(`Command '${command.name}' already exists.`);
command.isVencordCommand = true;
command.untranslatedName ??= command.name;
command.untranslatedDescription ??= command.description;
command.id ??= `-${BUILT_IN.length + 1}`;
command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT;

View file

@ -93,8 +93,10 @@ export interface Command {
isVencordCommand?: boolean;
name: string;
untranslatedName?: string;
displayName?: string;
description: string;
untranslatedDescription?: string;
displayDescription?: string;
options?: Option[];

View file

@ -90,19 +90,20 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
* A helper function for finding the children array of a group nested inside a context menu based on the id(s) of its children
* @param id The id of the child. If an array is specified, all ids will be tried
* @param children The context menu children
* @param matchSubstring Whether to check if the id is a substring of the child id
*/
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>): Array<ReactElement | null> | null {
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null | undefined>, matchSubstring = false): Array<ReactElement | null | undefined> | null {
for (const child of children) {
if (child == null) continue;
if (Array.isArray(child)) {
const found = findGroupChildrenByChildId(id, child);
const found = findGroupChildrenByChildId(id, child, matchSubstring);
if (found !== null) return found;
}
if (
(Array.isArray(id) && id.some(id => child.props?.id === id))
|| child.props?.id === id
(Array.isArray(id) && id.some(id => matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id))
|| (matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id)
) return children;
let nextChildren = child.props?.children;
@ -112,7 +113,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
child.props.children = nextChildren;
}
const found = findGroupChildrenByChildId(id, nextChildren);
const found = findGroupChildrenByChildId(id, nextChildren, matchSubstring);
if (found !== null) return found;
}
}

View file

@ -230,6 +230,10 @@ export function definePluginSettings<
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return Settings.plugins[definedSettings.pluginName] as any;
},
get plain() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return PlainSettings.plugins[definedSettings.pluginName] as any;
},
use: settings => useSettings(
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
).plugins[definedSettings.pluginName] as any,

View file

@ -65,8 +65,7 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
}
/**
* Discord's copy icon, as seen in the user popout right of the username when clicking
* your own username in the bottom left user panel
* Discord's copy icon, as seen in the user panel popout on the right of the username and in large code blocks
*/
export function CopyIcon(props: IconProps) {
return (
@ -76,8 +75,9 @@ export function CopyIcon(props: IconProps) {
viewBox="0 0 24 24"
>
<g fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" />
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" />
<path d="M3 16a1 1 0 0 1-1-1v-5a8 8 0 0 1 8-8h5a1 1 0 0 1 1 1v.5a.5.5 0 0 1-.5.5H10a6 6 0 0 0-6 6v5.5a.5.5 0 0 1-.5.5H3Z" />
<path d="M6 18a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4h-3a5 5 0 0 1-5-5V6h-4a4 4 0 0 0-4 4v8Z" />
<path d="M21.73 12a3 3 0 0 0-.6-.88l-4.25-4.24a3 3 0 0 0-.88-.61V9a3 3 0 0 0 3 3h2.73Z" />
</g>
</Icon>
);

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { OptionType, PluginOptionNumber } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
@ -54,7 +56,8 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<TextInput
type="number"
pattern="-?[0-9]+"

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSelect } from "@utils/types";
import { Forms, React, Select } from "@webpack/common";
@ -44,7 +46,8 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16} type="description">{option.description}</Forms.FormText>
<Select
isDisabled={option.disabled?.call(definedSettings) ?? false}
options={option.options}

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSlider } from "@utils/types";
import { Forms, React, Slider } from "@webpack/common";
@ -50,7 +52,8 @@ export function SettingSliderComponent({ option, pluginSettings, definedSettings
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<Slider
disabled={option.disabled?.call(definedSettings) ?? false}
markers={option.markers}

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionString } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
@ -41,7 +43,8 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<TextInput
type="text"
value={state}

View file

@ -93,7 +93,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = Settings.plugins[plugin.name];
const isEnabled = () => settings.enabled ?? false;
const isEnabled = () => Vencord.Plugins.isPluginEnabled(plugin.name);
function toggleEnabled() {
const wasEnabled = isEnabled();
@ -292,10 +292,10 @@ export default function PluginSettings() {
if (!pluginFilter(p)) continue;
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);
if (isRequired) {
const tooltipText = p.required
const tooltipText = p.required || !depMap[p.name]
? "This plugin is required for Vencord to function."
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));

View file

@ -382,6 +382,7 @@ function PatchHelper() {
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<CodeBlock lang="js" content={code} />
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
<Button className={Margins.top8} onClick={() => Clipboard.copy("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
</>
)}
</SettingsTab>

View file

@ -25,10 +25,9 @@ import { openPluginModal } from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack";
import { findLazy } from "@webpack";
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";
@ -45,9 +44,7 @@ type FileInput = ComponentType<{
filters?: { name?: string; extensions: string[]; }[];
}>;
const InviteActions = findByPropsLazy("resolveInvite");
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
const cl = classNameFactory("vc-settings-theme-");
@ -80,8 +77,16 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div>
{themeLinks.map(link => (
<Card style={{
{themeLinks.map(rawLink => {
const { label, link } = (() => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return { label: rawLink, link: rawLink };
const [, mode, link] = match;
return { label: `[${mode} mode only] ${link}`, link };
})();
return <Card style={{
padding: ".5em",
marginBottom: ".5em",
marginTop: ".5em"
@ -89,11 +94,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
}}>
{link}
{label}
</Forms.FormTitle>
<Validator link={link} />
</Card>
))}
</Card>;
})}
</div>
</>
);
@ -299,6 +304,7 @@ function ThemesTab() {
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
</Card>
@ -306,7 +312,7 @@ function ThemesTab() {
<TextArea
value={themeText}
onChange={setThemeText}
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")}
className={"vc-settings-theme-links"}
placeholder="Theme Links"
spellCheck={false}
onBlur={onBlur}

View file

@ -33,6 +33,20 @@
padding: 0.5em;
border: 1px solid var(--background-modifier-accent);
max-height: unset;
background-color: transparent;
box-sizing: border-box;
font-size: 12px;
line-height: 14px;
resize: none;
width: 100%;
}
.vc-settings-theme-links::placeholder {
color: var(--header-secondary);
}
.vc-settings-theme-links:focus {
background-color: var(--background-tertiary);
}
.vc-cloud-settings-sync-grid {

View file

@ -134,7 +134,7 @@ export async function loadLazyChunks() {
const allChunks = [] as number[];
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)")|(?:([\deE]+?):)/g)) {
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;

View file

@ -17,7 +17,7 @@
*/
import { onceDefined } from "@shared/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import electron, { app, BrowserWindowConstructorOptions, Menu, nativeTheme } from "electron";
import { dirname, join } from "path";
import { initIpc } from "./ipcMain";
@ -100,6 +100,19 @@ if (!IS_VANILLA) {
super(options);
initIpc(this);
// Workaround for https://github.com/electron/electron/issues/43367. Vesktop also has its own workaround
// @TODO: Remove this when the issue is fixed
if (IS_DISCORD_DESKTOP) {
this.webContents.on("devtools-opened", () => {
if (!nativeTheme.shouldUseDarkColors) return;
nativeTheme.themeSource = "light";
setTimeout(() => {
nativeTheme.themeSource = "dark";
}, 100);
});
}
} else super(options);
}
}

View file

@ -1,3 +0,0 @@
[class*="profileBadges"] {
flex: none;
}

View file

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./fixBadgeOverflow.css";
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
@ -62,34 +60,6 @@ export default definePlugin({
authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
required: true,
patches: [
/* Patch the badge list component on user profiles */
{
find: 'id:"premium",',
replacement: [
{
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
replace: "$&$1.unshift(...$self.getBadges(arguments[0]));",
},
{
// alt: "", aria-hidden: false, src: originalSrc
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/,
// ...badge.props, ..., src: badge.image ?? ...
replace: "...$1.props,$& $1.image??"
},
// replace their component with ours if applicable
{
match: /(?<=text:(\i)\.description,spacing:12,.{0,50})children:/,
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
},
// conditionally override their onClick with badge.onClick if it exists
{
match: /href:(\i)\.link/,
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
}
]
},
/* new profiles */
{
find: ".FULL_SIZE]:26",
replacement: {
@ -107,7 +77,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??"
},
{
match: /(?<=text:(\i)\.description,.{0,50})children:/,
match: /(?<=text:(\i)\.description,.{0,200})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
},
// conditionally override their onClick with badge.onClick if it exists

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "DynamicImageModalAPI",
authors: [Devs.sadan, Devs.Nuckyz],
description: "Allows you to omit either width or height when opening an image modal",
patches: [
{
find: "SCALE_DOWN:",
replacement: {
match: /!\(null==(\i)\|\|0===\i\|\|null==(\i)\|\|0===\i\)/,
replace: (_, width, height) => `!((null == ${width} || 0 === ${width}) && (null == ${height} || 0 === ${height}))`
}
}
]
});

View file

@ -34,7 +34,7 @@ export default definePlugin({
{
find: "Messages.SERVERS,children",
replacement: {
match: /(?<=Messages\.SERVERS,children:).+?default:return null\}\}\)/,
match: /(?<=Messages\.SERVERS,children:)\i\.map\(\i\)/,
replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)"
}
}

View file

@ -48,7 +48,7 @@ export default definePlugin({
},
},
{
find: ".METRICS,",
find: ".METRICS",
replacement: [
{
match: /this\._intervalId=/,

View file

@ -64,7 +64,7 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
},
{
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,30}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,60}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
}
]
@ -197,7 +197,7 @@ export default definePlugin({
},
get electronVersion() {
return VencordNative.native.getVersions().electron || window.armcord?.electron || null;
return VencordNative.native.getVersions().electron || window.legcord?.electron || null;
},
get chromiumVersion() {

View file

@ -77,7 +77,7 @@ async function generateDebugInfoMessage() {
const client = (() => {
if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;
if (IS_VESKTOP) return `Vesktop v${VesktopNative.app.getVersion()}`;
if ("armcord" in window) return `ArmCord v${window.armcord.version}`;
if ("legcord" in window) return `Legcord v${window.legcord.version}`;
// @ts-expect-error
const name = typeof unsafeWindow !== "undefined" ? "UserScript" : "Web";
@ -142,15 +142,15 @@ export default definePlugin({
required: true,
description: "Helps us provide support to you",
authors: [Devs.Ven],
dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"],
settings,
patches: [{
find: ".BEGINNING_DM.format",
replacement: {
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,300}(\i)\.isMultiUserDM)/,
replace: "$& $self.renderContributorDmWarningCard({ channel: $1 }),"
}
}],
@ -235,7 +235,8 @@ export default definePlugin({
}
},
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
const userId = channel.getRecipientId();
if (!isPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;

View file

@ -0,0 +1,7 @@
# AccountPanelServerProfile
Right click your account panel in the bottom left to view your profile in the current server
![](https://github.com/user-attachments/assets/3228497d-488f-479c-93d2-a32ccdb08f0f)
![](https://github.com/user-attachments/assets/6fc45363-d95f-4810-812f-2f9fb28b41b5)

View file

@ -0,0 +1,134 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ContextMenuApi, Menu, useEffect, useRef } from "@webpack/common";
import { User } from "discord-types/general";
interface UserProfileProps {
popoutProps: Record<string, any>;
currentUser: User;
originalPopout: () => React.ReactNode;
}
const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined");
const styles = findByPropsLazy("accountProfilePopoutWrapper");
let openAlternatePopout = false;
let accountPanelRef: React.MutableRefObject<Record<PropertyKey, any> | null> = { current: null };
const AccountPanelContextMenu = ErrorBoundary.wrap(() => {
const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]);
return (
<Menu.Menu
navId="vc-ap-server-profile"
onClose={ContextMenuApi.closeContextMenu}
>
<Menu.MenuItem
id="vc-ap-view-alternate-popout"
label={prioritizeServerProfile ? "View Account Profile" : "View Server Profile"}
disabled={getCurrentChannel()?.getGuildId() == null}
action={e => {
openAlternatePopout = true;
accountPanelRef.current?.props.onMouseDown();
accountPanelRef.current?.props.onClick(e);
}}
/>
<Menu.MenuCheckboxItem
id="vc-ap-prioritize-server-profile"
label="Prioritize Server Profile"
checked={prioritizeServerProfile}
action={() => settings.store.prioritizeServerProfile = !prioritizeServerProfile}
/>
</Menu.Menu>
);
}, { noop: true });
const settings = definePluginSettings({
prioritizeServerProfile: {
type: OptionType.BOOLEAN,
description: "Prioritize Server Profile when left clicking your account panel",
default: false
}
});
export default definePlugin({
name: "AccountPanelServerProfile",
description: "Right click your account panel in the bottom left to view your profile in the current server",
authors: [Devs.Nuckyz, Devs.relitrix],
settings,
patches: [
{
find: ".Messages.ACCOUNT_SPEAKING_WHILE_MUTED",
group: true,
replacement: [
{
match: /(?<=\.SIZE_32\)}\);)/,
replace: "$self.useAccountPanelRef();"
},
{
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalPopout:()=>{${originalPopout}}})`
},
{
match: /\.AVATAR,children:.+?(?=renderPopout:)/,
replace: "$&onRequestClose:$self.onPopoutClose,"
},
{
match: /(?<=.avatarWrapper,)/,
replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu,"
}
]
}
],
get accountPanelRef() {
return accountPanelRef;
},
useAccountPanelRef() {
useEffect(() => () => {
accountPanelRef.current = null;
}, []);
return (accountPanelRef = useRef(null));
},
openAccountPanelContextMenu(event: React.UIEvent) {
ContextMenuApi.openContextMenu(event, AccountPanelContextMenu);
},
onPopoutClose() {
openAlternatePopout = false;
},
UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalPopout }: UserProfileProps) => {
if (
(settings.store.prioritizeServerProfile && openAlternatePopout) ||
(!settings.store.prioritizeServerProfile && !openAlternatePopout)
) {
return originalPopout();
}
const currentChannel = getCurrentChannel();
if (currentChannel?.getGuildId() == null) {
return originalPopout();
}
return (
<div className={styles.accountProfilePopoutWrapper}>
<UserProfile {...popoutProps} userId={currentUser.id} guildId={currentChannel.getGuildId()} channelId={currentChannel.id} />
</div>
);
}, { noop: true })
});

View file

@ -0,0 +1,3 @@
# Always Expand Roles
Always expands the role list in profile popouts

View file

@ -1,6 +1,6 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
* 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
@ -16,20 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
migratePluginSettings("AlwaysExpandRoles", "ShowAllRoles");
export default definePlugin({
name: "TimeBarAllActivities",
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
authors: [Devs.fawn],
name: "AlwaysExpandRoles",
description: "Always expands the role list in profile popouts",
authors: [Devs.surgedevs],
patches: [
{
find: "}renderTimeBar(",
find: 'action:"EXPAND_ROLES"',
replacement: {
match: /renderTimeBar\((.{1,3})\){.{0,50}?let/,
replace: "renderTimeBar($1){let"
match: /(roles:\i(?=.+?(\i)\(!0\)[,;]\i\({action:"EXPAND_ROLES"}\)).+?\[\i,\2\]=\i\.useState\()!1\)/,
replace: (_, rest, setExpandedRoles) => `${rest}!0)`
}
}
],
]
});

View file

@ -71,7 +71,7 @@ export default definePlugin({
description: "Anonymise uploaded file names",
patches: [
{
find: "instantBatchUpload:function",
find: "instantBatchUpload:",
replacement: {
match: /uploadFiles:(\i),/,
replace:

View file

@ -24,7 +24,7 @@ interface ActivityButton {
}
interface Activity {
state: string;
state?: string;
details?: string;
timestamps?: {
start?: number;
@ -52,8 +52,8 @@ const enum ActivityFlag {
export interface TrackData {
name: string;
album: string;
artist: string;
album?: string;
artist?: string;
appleMusicLink?: string;
songLink?: string;
@ -61,8 +61,8 @@ export interface TrackData {
albumArtwork?: string;
artistArtwork?: string;
playerPosition: number;
duration: number;
playerPosition?: number;
duration?: number;
}
const enum AssetImageType {
@ -120,7 +120,7 @@ const settings = definePluginSettings({
stateString: {
type: OptionType.STRING,
description: "Activity state format string",
default: "{artist}"
default: "{artist} · {album}"
},
largeImageType: {
type: OptionType.SELECT,
@ -155,8 +155,8 @@ const settings = definePluginSettings({
function customFormat(formatStr: string, data: TrackData) {
return formatStr
.replaceAll("{name}", data.name)
.replaceAll("{album}", data.album)
.replaceAll("{artist}", data.artist);
.replaceAll("{album}", data.album ?? "")
.replaceAll("{artist}", data.artist ?? "");
}
function getImageAsset(type: AssetImageType, data: TrackData) {
@ -212,14 +212,16 @@ export default definePlugin({
const assets: ActivityAssets = {};
const isRadio = Number.isNaN(trackData.duration) && (trackData.playerPosition === 0);
if (settings.store.largeImageType !== AssetImageType.Disabled) {
assets.large_image = largeImageAsset;
assets.large_text = customFormat(settings.store.largeTextString, trackData);
if (!isRadio) assets.large_text = customFormat(settings.store.largeTextString, trackData);
}
if (settings.store.smallImageType !== AssetImageType.Disabled) {
assets.small_image = smallImageAsset;
assets.small_text = customFormat(settings.store.smallTextString, trackData);
if (!isRadio) assets.small_text = customFormat(settings.store.smallTextString, trackData);
}
const buttons: ActivityButton[] = [];
@ -243,17 +245,17 @@ export default definePlugin({
name: customFormat(settings.store.nameString, trackData),
details: customFormat(settings.store.detailsString, trackData),
state: customFormat(settings.store.stateString, trackData),
state: isRadio ? undefined : customFormat(settings.store.stateString, trackData),
timestamps: (settings.store.enableTimestamps ? {
timestamps: (trackData.playerPosition && trackData.duration && settings.store.enableTimestamps) ? {
start: Date.now() - (trackData.playerPosition * 1000),
end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
} : undefined),
} : undefined,
assets,
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
metadata: { button_urls: buttons.map(v => v.url) || undefined, },
buttons: !isRadio && buttons.length ? buttons.map(v => v.label) : undefined,
metadata: !isRadio && buttons.length ? { button_urls: buttons.map(v => v.url) } : undefined,
type: settings.store.activityType,
flags: ActivityFlag.INSTANCE,

View file

@ -11,37 +11,11 @@ import type { TrackData } from ".";
const exec = promisify(execFile);
// function exec(file: string, args: string[] = []) {
// return new Promise<{ code: number | null, stdout: string | null, stderr: string | null; }>((resolve, reject) => {
// const process = spawn(file, args, { stdio: [null, "pipe", "pipe"] });
// let stdout: string | null = null;
// process.stdout.on("data", (chunk: string) => { stdout ??= ""; stdout += chunk; });
// let stderr: string | null = null;
// process.stderr.on("data", (chunk: string) => { stdout ??= ""; stderr += chunk; });
// process.on("exit", code => { resolve({ code, stdout, stderr }); });
// process.on("error", err => reject(err));
// });
// }
async function applescript(cmds: string[]) {
const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
return stdout;
}
function makeSearchUrl(type: string, query: string) {
const url = new URL("https://tools.applemediaservices.com/api/apple-media/music/US/search.json");
url.searchParams.set("types", type);
url.searchParams.set("limit", "1");
url.searchParams.set("term", query);
return url;
}
const requestOptions: RequestInit = {
headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; rv:125.0) Gecko/20100101 Firefox/125.0" },
};
interface RemoteData {
appleMusicLink?: string,
songLink?: string,
@ -51,6 +25,24 @@ interface RemoteData {
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
const APPLE_MUSIC_BUNDLE_REGEX = /<script type="module" crossorigin src="([a-zA-Z0-9.\-/]+)"><\/script>/;
const APPLE_MUSIC_TOKEN_REGEX = /\w+="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)",\w+="x-apple-jingle-correlation-key"/;
let cachedToken: string | undefined = undefined;
const getToken = async () => {
if (cachedToken) return cachedToken;
const html = await fetch("https://music.apple.com/").then(r => r.text());
const bundleUrl = new URL(html.match(APPLE_MUSIC_BUNDLE_REGEX)![1], "https://music.apple.com/");
const bundle = await fetch(bundleUrl).then(r => r.text());
const token = bundle.match(APPLE_MUSIC_TOKEN_REGEX)![1];
cachedToken = token;
return token;
};
async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
if (id === cachedRemoteData?.id) {
if ("data" in cachedRemoteData) return cachedRemoteData.data;
@ -58,21 +50,39 @@ async function fetchRemoteData({ id, name, artist, album }: { id: string, name:
}
try {
const [songData, artistData] = await Promise.all([
fetch(makeSearchUrl("songs", artist + " " + album + " " + name), requestOptions).then(r => r.json()),
fetch(makeSearchUrl("artists", artist.split(/ *[,&] */)[0]), requestOptions).then(r => r.json())
]);
const dataUrl = new URL("https://amp-api-edge.music.apple.com/v1/catalog/us/search");
dataUrl.searchParams.set("platform", "web");
dataUrl.searchParams.set("l", "en-US");
dataUrl.searchParams.set("limit", "1");
dataUrl.searchParams.set("with", "serverBubbles");
dataUrl.searchParams.set("types", "songs");
dataUrl.searchParams.set("term", `${name} ${artist} ${album}`);
dataUrl.searchParams.set("include[songs]", "artists");
const appleMusicLink = songData?.songs?.data[0]?.attributes.url;
const songLink = songData?.songs?.data[0]?.id ? `https://song.link/i/${songData?.songs?.data[0]?.id}` : undefined;
const token = await getToken();
const albumArtwork = songData?.songs?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
const artistArtwork = artistData?.artists?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
const songData = await fetch(dataUrl, {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"authorization": `Bearer ${token}`,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"origin": "https://music.apple.com",
},
})
.then(r => r.json())
.then(data => data.results.song.data[0]);
cachedRemoteData = {
id,
data: { appleMusicLink, songLink, albumArtwork, artistArtwork }
data: {
appleMusicLink: songData.attributes.url,
songLink: `https://song.link/i/${songData.id}`,
albumArtwork: songData.attributes.artwork.url.replace("{w}x{h}", "512x512"),
artistArtwork: songData.relationships.artists.data[0].attributes.artwork.url.replace("{w}x{h}", "512x512"),
}
};
return cachedRemoteData.data;
} catch (e) {
console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);

View file

@ -73,8 +73,8 @@ export default definePlugin({
},
async start() {
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users
if ("armcord" in window) return;
// Legcord comes with its own arRPC implementation, so this plugin just confuses users
if ("legcord" in window) return;
if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket

View file

@ -1,5 +0,0 @@
# AutomodContext
Allows you to jump to the messages surrounding an automod hit
![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/d13740c8-2062-4553-b975-82fd3d6cc08b)

View file

@ -1,73 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Text } from "@webpack/common";
const { selectChannel } = findByPropsLazy("selectChannel", "selectVoiceChannel");
function jumpToMessage(channelId: string, messageId: string) {
const guildId = ChannelStore.getChannel(channelId)?.guild_id;
selectChannel({
guildId,
channelId,
messageId,
jumpType: "INSTANT"
});
}
function findChannelId(message: any): string | null {
const { embeds: [embed] } = message;
const channelField = embed.fields.find(({ rawName }) => rawName === "channel_id");
if (!channelField) {
return null;
}
return channelField.rawValue;
}
export default definePlugin({
name: "AutomodContext",
description: "Allows you to jump to the messages surrounding an automod hit.",
authors: [Devs.JohnyTheCarrot],
patches: [
{
find: ".Messages.GUILD_AUTOMOD_REPORT_ISSUES",
replacement: {
match: /\.Messages\.ACTIONS.+?}\)(?=,(\(0.{0,40}\.dot.*?}\)),)/,
replace: (m, dot) => `${m},${dot},$self.renderJumpButton({message:arguments[0].message})`
}
}
],
renderJumpButton: ErrorBoundary.wrap(({ message }: { message: any; }) => {
const channelId = findChannelId(message);
if (!channelId) {
return null;
}
return (
<Button
style={{ padding: "2px 8px" }}
look={Button.Looks.LINK}
size={Button.Sizes.SMALL}
color={Button.Colors.LINK}
onClick={() => jumpToMessage(channelId, message.id)}
>
<Text color="text-link" variant="text-xs/normal">
Jump to Surrounding
</Text>
</Button>
);
}, { noop: true })
});

View file

@ -0,0 +1,11 @@
# Better Folders
Better Folders offers a variety of options to improve your folder experience
Always show the folder icon, regardless of if the folder is open or not
Only have one folder open at a time
Open folders in a sidebar:
![A folder open in a separate sidebar](https://github.com/user-attachments/assets/432d3146-8091-4bae-9c1e-c19046c72947)

View file

@ -30,9 +30,9 @@ enum FolderIconDisplay {
MoreThanOneFolderExpanded
}
const GuildsTree = findLazy(m => m.prototype?.moveNextTo);
const SortedGuildStore = findStoreLazy("SortedGuildStore");
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const SortedGuildStore = findStoreLazy("SortedGuildStore");
const GuildsTree = findLazy(m => m.prototype?.moveNextTo);
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
let lastGuildId = null as string | null;
@ -118,22 +118,22 @@ export default definePlugin({
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
{
match: /\[(\i)\]=(\(0,\i\.\i\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0]?.isBetterFolders,betterFoldersOriginalTree,arguments[0]?.betterFoldersExpandedIds)`
},
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{
match: /lastTargetNode:\i\[\i\.length-1\].+?Fragment.+?\]}\)\]/,
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0].isBetterFolders))"
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))"
},
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
{
match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0]?.isBetterFolders))"
},
// Export the isBetterFolders variable to the folders component
{
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,)/,
replace: 'isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/,
replace: '$&isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
}
]
},
@ -167,31 +167,31 @@ export default definePlugin({
{
predicate: () => settings.store.keepIcons,
match: /(?<=let{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;)(?=let)/,
replace: (_, isExpanded) => `${isExpanded}=!!arguments[0].isBetterFolders&&${isExpanded};`
replace: (_, isExpanded) => `${isExpanded}=!!arguments[0]?.isBetterFolders&&${isExpanded};`
},
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{
predicate: () => !settings.store.keepIcons,
match: /(?<=\.Messages\.SERVER_FOLDER_PLACEHOLDER.+?useTransition\)\()/,
replace: "!!arguments[0].isBetterFolders&&"
replace: "$self.shouldShowTransition(arguments[0])&&"
},
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
{
predicate: () => !settings.store.keepIcons,
match: /expandedFolderBackground,.+?,(?=\i\(\(\i,\i,\i\)=>{let{key.{0,45}ul)(?<=selected:\i,expanded:(\i),.+?)/,
replace: (m, isExpanded) => `${m}!arguments[0].isBetterFolders&&${isExpanded}?null:`
replace: (m, isExpanded) => `${m}$self.shouldRenderContents(arguments[0],${isExpanded})?null:`
},
{
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.wrapper,children:\[)/,
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0].isBetterFolders,arguments[0].betterFoldersExpandedIds)&&"
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
},
{
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.expandedFolderBackground.+?}\),)(?=\i,)/,
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0].isBetterFolders,arguments[0].betterFoldersExpandedIds)?null:"
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)?null:"
}
]
},
@ -200,8 +200,8 @@ export default definePlugin({
predicate: () => settings.store.sidebar,
replacement: {
// Render the Better Folders sidebar
match: /(?<=({className:\i\.guilds,themeOverride:\i})\))/,
replace: ",$self.FolderSideBar($1)"
match: /(container.{0,50}({className:\i\.guilds,themeOverride:\i})\))/,
replace: "$1,$self.FolderSideBar({...$2})"
}
},
{
@ -306,7 +306,20 @@ export default definePlugin({
}
},
FolderSideBar: guildsBarProps => <FolderSideBar {...guildsBarProps} />,
shouldShowTransition(props: any) {
// Pending guilds
if (props?.folderNode?.id === 1) return true;
closeFolders
return !!props?.isBetterFolders;
},
shouldRenderContents(props: any, isExpanded: boolean) {
// Pending guilds
if (props?.folderNode?.id === 1) return false;
return !props?.isBetterFolders && isExpanded;
},
FolderSideBar,
closeFolders,
});

View file

@ -17,13 +17,9 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
const UserPopoutSectionCssClasses = findByPropsLazy("section", "lastSection");
const settings = definePluginSettings({
hide: {
@ -72,23 +68,9 @@ export default definePlugin({
match: /\.NOTE_PLACEHOLDER,/,
replace: "$&spellCheck:!$self.noSpellCheck,"
}
},
{
find: ".popularApplicationCommandIds,",
replacement: {
match: /lastSection:(!?\i)}\),/,
replace: "$&$self.patchPadding({lastSection:$1}),"
}
}
],
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
if (!lastSection) return null;
return (
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
);
}),
get noSpellCheck() {
return settings.store.noSpellCheck;
}

View file

@ -99,7 +99,11 @@ export default definePlugin({
id="vc-view-role-icon"
label="View Role Icon"
action={() => {
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
openImageModal({
url: `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`,
height: 128,
width: 128
});
}}
icon={ImageIcon}
/>

View file

@ -0,0 +1,68 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { isObjectEmpty } from "@utils/misc";
import { Alerts, i18n, Menu, useMemo, useState } from "@webpack/common";
import Plugins from "~plugins";
function onRestartNeeded() {
Alerts.show({
title: "Restart required",
body: <p>You have changed settings that require a restart.</p>,
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: () => location.reload()
});
}
export default function PluginsSubmenu() {
const sortedPlugins = useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []);
const [query, setQuery] = useState("");
const search = query.toLowerCase();
const include = (p: typeof Plugins[keyof typeof Plugins]) => (
Vencord.Plugins.isPluginEnabled(p.name)
&& p.options && !isObjectEmpty(p.options)
&& (
p.name.toLowerCase().includes(search)
|| p.description.toLowerCase().includes(search)
|| p.tags?.some(t => t.toLowerCase().includes(search))
)
);
const plugins = sortedPlugins.filter(include);
return (
<>
<Menu.MenuControlItem
id="vc-plugins-search"
control={(props, ref) => (
<Menu.MenuSearchControl
{...props}
query={query}
onChange={setQuery}
ref={ref}
placeholder={i18n.Messages.SEARCH}
/>
)}
/>
{!!plugins.length && <Menu.MenuSeparator />}
{plugins.map(p => (
<Menu.MenuItem
key={p.name}
id={p.name}
label={p.name}
action={() => openPluginModal(p, onRestartNeeded)}
/>
))}
</>
);
}

View file

@ -13,6 +13,8 @@ import { waitFor } from "@webpack";
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
import type { HTMLAttributes, ReactElement } from "react";
import PluginsSubmenu from "./PluginsSubmenu";
type SettingsEntry = { section: string, label: string; };
const cl = classNameFactory("");
@ -118,13 +120,21 @@ export default definePlugin({
},
{ // Settings cog context menu
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
replacement: [
{
match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$1$self.wrapMenu($2)"
},
{
match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/,
replace: "$&case 'VencordPlugins':return $self.PluginsSubmenu();"
}
}
]
},
],
PluginsSubmenu,
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
// without possibly also catching unrelated errors of children.
//

View file

@ -57,7 +57,11 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica
const previewUrl = await ApplicationStreamPreviewStore.getPreviewURL(guildId, channelId, ownerId);
if (!previewUrl) return;
openImageModal(previewUrl);
openImageModal({
url: previewUrl,
height: 720,
width: 1280
});
};
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => {

View file

@ -45,8 +45,8 @@ export default definePlugin({
{
find: ".embedWrapper,embed",
replacement: [{
match: /\.embedWrapper(?=.+?channel_id:(\i)\.id)/g,
replace: "$&+($1.nsfw?' vc-nsfw-img':'')"
match: /\.container/,
replace: "$&+(this.props.channel.nsfw? ' vc-nsfw-img': '')"
}]
}
],

View file

@ -155,4 +155,5 @@ export const defaultRules = [
"igshid",
"igsh",
"share_id@reddit.com",
"si@soundcloud.com",
];

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, useStateFromStores } from "@webpack/common";
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
@ -36,7 +36,6 @@ function setTheme(theme: string) {
saveClientTheme({ theme });
}
const ThemeStore = findStoreLazy("ThemeStore");
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
function ThemeSettings() {

View file

@ -1,5 +1,5 @@
# ConsoleJanitor
Disables annoying console messages/errors. This plugin mainly removes errors/warnings that happen all the time and noisy/spammy logging messages.
Disables annoying console messages/errors. This plugin mainly removes errors/warnings that happen all the time and Discord logger messages.
Some of the disabled messages include the "notosans-400-normalitalic" error and MessageActionCreators, Routing/Utils loggers.
One of the disabled messages is the "Window state not initialized" warning, for example.

View file

@ -6,7 +6,7 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin, { OptionType, StartAt } from "@utils/types";
const Noop = () => { };
const NoopLogger = {
@ -22,10 +22,12 @@ const NoopLogger = {
fileOnly: Noop
};
const logAllow = new Set();
const settings = definePluginSettings({
disableNoisyLoggers: {
disableLoggers: {
type: OptionType.BOOLEAN,
description: "Disable noisy loggers like the MessageActionCreators",
description: "Disables Discords loggers",
default: false,
restartNeeded: true
},
@ -34,18 +36,43 @@ const settings = definePluginSettings({
description: "Disable the Spotify logger, which leaks account information and access token",
default: true,
restartNeeded: true
},
whitelistedLoggers: {
type: OptionType.STRING,
description: "Semi colon separated list of loggers to allow even if others are hidden",
default: "GatewaySocket; Routing/Utils",
onChange(newVal: string) {
logAllow.clear();
newVal.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
}
}
});
export default definePlugin({
name: "ConsoleJanitor",
description: "Disables annoying console messages/errors",
authors: [Devs.Nuckyz],
authors: [Devs.Nuckyz, Devs.sadan],
settings,
startAt: StartAt.Init,
start() {
logAllow.clear();
this.settings.store.whitelistedLoggers?.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
},
NoopLogger: () => NoopLogger,
shouldLog(logger: string) {
return logAllow.has(logger);
},
patches: [
{
find: 'react-spring: The "interpolate" function',
replacement: {
match: /,console.warn\('react-spring: The "interpolate" function is deprecated in v10 \(use "to" instead\)'\)/,
replace: ""
}
},
{
find: 'console.warn("Window state not initialized"',
replacement: {
@ -60,13 +87,6 @@ export default definePlugin({
replace: ""
}
},
{
find: "notosans-400-normalitalic",
replacement: {
match: /,"notosans-.+?"/g,
replace: ""
}
},
{
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
all: true,
@ -110,34 +130,13 @@ export default definePlugin({
replace: ""
}
},
...[
'("MessageActionCreators")', '("ChannelMessages")',
'("Routing/Utils")', '("RTCControlSocket")',
'("ConnectionEventFramerateReducer")', '("RTCLatencyTestManager")',
'("OverlayBridgeStore")', '("RPCServer:WSS")', '("RPCServer:IPC")'
].map(logger => ({
find: logger,
predicate: () => settings.store.disableNoisyLoggers,
all: true,
replacement: {
match: new RegExp(String.raw`new \i\.\i${logger.replace(/([()])/g, "\\$1")}`),
replace: `$self.NoopLogger${logger}`
}
})),
// Patches discords generic logger function
{
find: '"Experimental codecs: "',
predicate: () => settings.store.disableNoisyLoggers,
find: "Σ:",
predicate: () => settings.store.disableLoggers,
replacement: {
match: /new \i\.\i\("Connection\("\.concat\(\i,"\)"\)\)/,
replace: "$self.NoopLogger()"
}
},
{
find: '"Handling ping: "',
predicate: () => settings.store.disableNoisyLoggers,
replacement: {
match: /new \i\.\i\("RTCConnection\("\.concat.+?\)\)(?=,)/,
replace: "$self.NoopLogger()"
match: /(?<=&&)(?=console)/,
replace: "$self.shouldLog(arguments[0])&&"
}
},
{
@ -148,5 +147,5 @@ export default definePlugin({
replace: "$self.NoopLogger()"
}
}
]
],
});

View file

@ -0,0 +1,5 @@
# CopyFileContents
Adds a button to text file attachments to copy their contents.
![](https://github.com/user-attachments/assets/b1a0f6f4-106f-4953-94d9-4c5ef5810bca)

View file

@ -0,0 +1,60 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { CopyIcon, NoEntrySignIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc";
import definePlugin from "@utils/types";
import { Tooltip, useState } from "@webpack/common";
const CheckMarkIcon = () => {
return <svg width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M21.7 5.3a1 1 0 0 1 0 1.4l-12 12a1 1 0 0 1-1.4 0l-6-6a1 1 0 1 1 1.4-1.4L9 16.58l11.3-11.3a1 1 0 0 1 1.4 0Z"></path>
</svg>;
};
export default definePlugin({
name: "CopyFileContents",
description: "Adds a button to text file attachments to copy their contents",
authors: [Devs.Obsidian, Devs.Nuckyz],
patches: [
{
find: ".Messages.PREVIEW_BYTES_LEFT.format(",
replacement: {
match: /\.footerGap.+?url:\i,fileName:\i,fileSize:\i}\),(?<=fileContents:(\i),bytesLeft:(\i).+?)/g,
replace: "$&$self.addCopyButton({fileContents:$1,bytesLeft:$2}),"
}
}
],
addCopyButton: ErrorBoundary.wrap(({ fileContents, bytesLeft }: { fileContents: string, bytesLeft: number; }) => {
const [recentlyCopied, setRecentlyCopied] = useState(false);
return (
<Tooltip text={recentlyCopied ? "Copied!" : bytesLeft > 0 ? "File too large to copy" : "Copy File Contents"}>
{tooltipProps => (
<div
{...tooltipProps}
className="vc-cfc-button"
role="button"
onClick={() => {
if (!recentlyCopied && bytesLeft <= 0) {
copyWithToast(fileContents);
setRecentlyCopied(true);
setTimeout(() => setRecentlyCopied(false), 2000);
}
}}
>
{recentlyCopied ? <CheckMarkIcon /> : bytesLeft > 0 ? <NoEntrySignIcon color="var(--channel-icon)" /> : <CopyIcon />}
</div>
)}
</Tooltip>
);
}, { noop: true }),
});

View file

@ -0,0 +1,9 @@
.vc-cfc-button {
color: var(--interactive-normal);
cursor: pointer;
padding-left: 4px;
}
.vc-cfc-button:hover {
color: var(--interactive-hover);
}

View file

@ -175,7 +175,7 @@ export default definePlugin({
}
if (settings.store.attemptToNavigateToHome) {
try {
NavigationRouter.transitionTo("/channels/@me");
NavigationRouter.transitionToGuild("@me");
} catch (err) {
CrashHandlerLogger.debug("Failed to navigate to home", err);
}

View file

@ -26,12 +26,11 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
@ -436,8 +435,8 @@ export default definePlugin({
<Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle }}>
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
<div style={{ width: "284px", ...profileThemeStyle, padding: 8, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
{activity[0] && <ActivityComponent activity={activity[0]} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
application={{ id: settings.store.appID }}
user={UserStore.getCurrentUser()} />}

View file

@ -37,8 +37,8 @@ export default definePlugin({
find: 'type:"IDLE",idle:',
replacement: [
{
match: /(?<=Date\.now\(\)-\i>)\i\.\i/,
replace: "$self.getIdleTimeout()"
match: /(?<=Date\.now\(\)-\i>)\i\.\i\|\|/,
replace: "$self.getIdleTimeout()||"
},
{
match: /Math\.min\((\i\.\i\.getSetting\(\)\*\i\.\i\.\i\.SECOND),\i\.\i\)/,

View file

@ -46,7 +46,7 @@ const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
async function embedDidMount(this: Component<Props>) {
try {
const { embed } = this.props;
const { replaceElements } = settings.store;
const { replaceElements, dearrowByDefault } = settings.store;
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
@ -63,18 +63,22 @@ async function embedDidMount(this: Component<Props>) {
if (!hasTitle && !hasThumb) return;
embed.dearrow = {
enabled: true
enabled: dearrowByDefault
};
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
}
const replacementTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
embed.dearrow.oldTitle = dearrowByDefault ? embed.rawTitle : replacementTitle;
if (dearrowByDefault) embed.rawTitle = replacementTitle;
}
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
const replacementProxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
embed.dearrow.oldThumb = dearrowByDefault ? embed.thumbnail.proxyURL : replacementProxyURL;
if (dearrowByDefault) embed.thumbnail.proxyURL = replacementProxyURL;
}
this.forceUpdate();
@ -96,6 +100,7 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
onClick={() => {
const { enabled, oldThumb, oldTitle } = embed.dearrow;
settings.store.dearrowByDefault = !enabled;
embed.dearrow.enabled = !enabled;
if (oldTitle) {
embed.dearrow.oldTitle = embed.rawTitle;
@ -153,6 +158,12 @@ const settings = definePluginSettings({
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
],
},
dearrowByDefault: {
description: "Dearrow videos automatically",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
}
});

View file

@ -54,7 +54,7 @@ export default definePlugin({
replace: "$self.DecorationGridItem=$&"
},
{
match: /(?<==)\i=>{let{user:\i,avatarDecoration.{300,600}decorationGridItemChurned/,
match: /(?<==)\i=>{let{user:\i,avatarDecoration/,
replace: "$self.DecorationGridDecoration=$&"
},
// Remove NEW label from decor avatar decorations
@ -91,7 +91,7 @@ export default definePlugin({
replacement: [
// Use Decor avatar decoration hook
{
match: /(?<=\i\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/,
match: /(?<=\i\)\({avatarDecoration:)(\i)(?=,)(?<=currentUser:(\i).+?)/,
replace: "$self.useUserDecorAvatarDecoration($1)??$&"
}
]

View file

@ -23,12 +23,13 @@ import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { findByPropsLazy, findLazy } from "@webpack";
import { Forms, React } from "@webpack/common";
import hideBugReport from "./hideBugReport.css?managed";
const KbdStyles = findByPropsLazy("key", "combo");
const BugReporterExperiment = findLazy(m => m?.definition?.id === "2024-09_bug_reporter");
const settings = definePluginSettings({
toolbarDevMenu: {
@ -78,8 +79,8 @@ export default definePlugin({
{
find: "toolbar:function",
replacement: {
match: /\i\.isStaff\(\)/,
replace: "true"
match: /hasBugReporterAccess:(\i)/,
replace: "_hasBugReporterAccess:$1=true"
},
predicate: () => settings.store.toolbarDevMenu
},
@ -91,10 +92,18 @@ export default definePlugin({
match: /\i\.isDM\(\)\|\|\i\.isThread\(\)/,
replace: "false",
}
},
// enable option to always record clips even if you are not streaming
{
find: "isDecoupledGameClippingEnabled(){",
replacement: {
match: /\i\.isStaff\(\)/,
replace: "true"
}
}
],
start: () => enableStyle(hideBugReport),
start: () => !BugReporterExperiment.getCurrentConfig().hasBugReporterAccess && enableStyle(hideBugReport),
stop: () => disableStyle(hideBugReport),
settingsAboutComponent: () => {

View file

@ -22,7 +22,7 @@ import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin, { OptionType, Patch } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Emoji } from "@webpack/types";
@ -194,6 +194,26 @@ const hasExternalStickerPerms = (channelId: string) => hasPermission(channelId,
const hasEmbedPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.EMBED_LINKS);
const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.ATTACH_FILES);
function makeBypassPatches(): Omit<Patch, "plugin"> {
const mapping: Array<{ func: string, predicate?: () => boolean; }> = [
{ func: "canUseCustomStickersEverywhere", predicate: () => settings.store.enableStickerBypass },
{ func: "canUseHighVideoUploadQuality", predicate: () => settings.store.enableStreamQualityBypass },
{ func: "canStreamQuality", predicate: () => settings.store.enableStreamQualityBypass },
{ func: "canUseClientThemes" },
{ func: "canUseCustomNotificationSounds" },
{ func: "canUsePremiumAppIcons" }
];
return {
find: "canUseCustomStickersEverywhere:",
replacement: mapping.map(({ func, predicate }) => ({
match: new RegExp(String.raw`(?<=${func}:function\(\i(?:,\i)?\){)`),
replace: "return true;",
predicate
}))
};
}
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
@ -203,6 +223,17 @@ export default definePlugin({
settings,
patches: [
// General bypass patches
makeBypassPatches(),
// Patch the emoji picker in voice calls to not be bypassed by fake nitro
{
find: "emojiItemDisabled]",
predicate: () => settings.store.enableEmojiBypass,
replacement: {
match: /CHAT/,
replace: "STATUS"
}
},
{
find: ".PREMIUM_LOCKED;",
group: true,
@ -243,15 +274,6 @@ export default definePlugin({
replace: (_, rest1, rest2) => `${rest1},fakeNitroOriginal){if(!fakeNitroOriginal)return false;${rest2}`
}
},
// Allow stickers to be sent everywhere
{
find: "canUseCustomStickersEverywhere:function",
predicate: () => settings.store.enableStickerBypass,
replacement: {
match: /canUseCustomStickersEverywhere:function\(\i\){/,
replace: "$&return true;"
},
},
// Make stickers always available
{
find: '"SENDABLE"',
@ -261,20 +283,6 @@ export default definePlugin({
replace: "true?"
}
},
// Allow streaming with high quality
{
find: "canUseHighVideoUploadQuality:function",
predicate: () => settings.store.enableStreamQualityBypass,
replacement: [
"canUseHighVideoUploadQuality",
"canStreamQuality",
].map(func => {
return {
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
replace: "$&return true;"
};
})
},
// Remove boost requirements to stream with high quality
{
find: "STREAM_FPS_OPTION.format",
@ -284,14 +292,6 @@ export default definePlugin({
replace: ""
}
},
// Allow client themes to be changeable
{
find: "canUseClientThemes:function",
replacement: {
match: /canUseClientThemes:function\(\i\){/,
replace: "$&return true;"
}
},
{
find: '"UserSettingsProtoStore"',
replacement: [
@ -350,7 +350,7 @@ export default definePlugin({
{
// Filter attachments to remove fake nitro stickers or emojis
predicate: () => settings.store.transformStickers,
match: /renderAttachments\(\i\){let{attachments:(\i).+?;/,
match: /renderAttachments\(\i\){.+?{attachments:(\i).+?;/,
replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`
}
]
@ -389,14 +389,6 @@ export default definePlugin({
replace: (_, reactNode, props) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!${props}?.fakeNitroNode?.fake)`
}
},
// Allow using custom app icons
{
find: "canUsePremiumAppIcons:function",
replacement: {
match: /canUsePremiumAppIcons:function\(\i\){/,
replace: "$&return true;"
}
},
// Separate patch for allowing using custom app icons
{
find: /\.getCurrentDesktopIcon.{0,25}\.isPremium/,
@ -412,14 +404,6 @@ export default definePlugin({
match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g,
replace: "true"
}
},
// Allow using custom notification sounds
{
find: "canUseCustomNotificationSounds:function",
replacement: {
match: /canUseCustomNotificationSounds:function\(\i\){/,
replace: "$&return true;"
}
}
],

View file

@ -121,7 +121,7 @@ export default definePlugin({
{
find: "UserProfileStore",
replacement: {
match: /(?<=getUserProfile\(\i\){return )(\i\[\i\])/,
match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/,
replace: "$self.colorDecodeHook($1)"
}
},

View file

@ -0,0 +1,3 @@
# Fix Images Quality
Prevents images from being loaded as webp, which can cause quality loss

View file

@ -8,16 +8,15 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "NoDefaultHangStatus",
description: "Disable the default hang status when joining voice channels",
authors: [Devs.D3SOX],
name: "FixImagesQuality",
description: "Prevents images from being loaded as webp, which can cause quality loss",
authors: [Devs.Nuckyz],
patches: [
{
find: ".CHILLING)",
find: "getFormatQuality(){",
replacement: {
match: /{enableHangStatus:(\i),/,
replace: "{_enableHangStatus:$1=false,"
match: /(?<=null;return )\i\.\i&&\(\i\|\|!\i\.isAnimated.+?:(?=\i&&\(\i="png"\))/,
replace: ""
}
}
]

View file

@ -27,7 +27,6 @@ export default definePlugin({
name: "FriendInvites",
description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).",
authors: [Devs.afn, Devs.Dziurwa],
dependencies: ["CommandsAPI"],
commands: [
{
name: "create friend invite",

View file

@ -7,121 +7,47 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { Heading, RelationshipStore, Text } from "@webpack/common";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { RelationshipStore, Text } from "@webpack/common";
const containerWrapper = findByPropsLazy("memberSinceWrapper");
const container = findByPropsLazy("memberSince");
const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"');
const locale = findByPropsLazy("getLocale");
const lastSection = findByPropsLazy("lastSection");
const section = findLazy((m: any) => m.section !== void 0 && m.heading !== void 0 && Object.values(m).length === 2);
const Section = findComponentByCodeLazy('"auto":"smooth"', ".section");
export default definePlugin({
name: "FriendsSince",
description: "Shows when you became friends with someone in the user popout",
authors: [Devs.Elvyra, Devs.Antti],
patches: [
// User popup - old layout
{
find: ".USER_PROFILE}};return",
replacement: {
match: /,{userId:(\i.id).{0,30}}\)/,
replace: "$&,$self.friendsSinceOld({ userId: $1 })"
}
},
// DM User Sidebar - old layout
{
find: ".PROFILE_PANEL,",
replacement: {
match: /,{userId:([^,]+?)}\)/,
replace: "$&,$self.friendsSinceOld({ userId: $1 })"
}
},
// User Profile Modal - old layout
{
find: ".userInfoSectionHeader,",
replacement: {
match: /(\.Messages\.USER_PROFILE_MEMBER_SINCE.+?userId:(.+?),textClassName:)(\i\.userInfoText)}\)/,
replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSinceOld({ userId: ${userId}, textClassName: ${textClassName} })`
}
},
// DM User Sidebar - new layout
// DM User Sidebar
{
find: ".PANEL}),nicknameIcons",
replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id)}\)}\)/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:true})"
replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:true})"
}
},
// User Profile Modal - new layout
// User Profile Modal
{
find: "action:\"PRESS_APP_CONNECTION\"",
replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id),.{0,100}}\)}\),/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:false}),"
replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false}),"
}
}
],
getFriendSince(userId: string) {
try {
if (!RelationshipStore.isFriend(userId)) return null;
return RelationshipStore.getSince(userId);
} catch (err) {
new Logger("FriendsSince").error(err);
return null;
}
},
friendsSinceOld: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
FriendsSinceComponent: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {
if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null;
return (
<div className={lastSection.section}>
<Heading variant="eyebrow">
Friends Since
</Heading>
<div className={containerWrapper.memberSinceWrapper}>
{!!getCurrentChannel()?.guild_id && (
<svg
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 24 24"
fill="var(--interactive-normal)"
>
<path d="M13 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" />
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
</svg>
)}
<Text variant="text-sm/normal" className={textClassName}>
{getCreatedAtDate(friendsSince, locale.getLocale())}
</Text>
</div>
</div>
);
}, { noop: true }),
friendsSinceNew: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {
if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null;
return (
<section className={section.section}>
<Heading variant="text-xs/semibold" style={isSidebar ? {} : { color: "var(--header-secondary)" }}>
Friends Since
</Heading>
<Section heading="Friends Since">
{
isSidebar ? (
<Text variant="text-sm/normal">
@ -149,8 +75,7 @@ export default definePlugin({
</div>
)
}
</section>
</Section>
);
}, { noop: true }),
});

View file

@ -0,0 +1,5 @@
# FullSearchContext
Makes the message context menu in message search results have all options you'd expect.
![](https://github.com/user-attachments/assets/472d1327-3935-44c7-b7c4-0978b5348550)

View file

@ -0,0 +1,111 @@
/*
* 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 { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { NoopComponent } from "@utils/react";
import definePlugin from "@utils/types";
import { filters, findByPropsLazy, waitFor } from "@webpack";
import { ChannelStore, ContextMenuApi, i18n, UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
const { useMessageMenu } = findByPropsLazy("useMessageMenu");
interface CopyIdMenuItemProps {
id: string;
label: string;
}
let CopyIdMenuItem: (props: CopyIdMenuItemProps) => React.ReactElement | null = NoopComponent;
waitFor(filters.componentByCode('"devmode-copy-id-".concat'), m => CopyIdMenuItem = m);
function MessageMenu({ message, channel, onHeightUpdate }) {
const canReport = message.author &&
!(message.author.id === UserStore.getCurrentUser().id || message.author.system);
return useMessageMenu({
navId: "message-actions",
ariaLabel: i18n.Messages.MESSAGE_UTILITIES_A11Y_LABEL,
message,
channel,
canReport,
onHeightUpdate,
onClose: () => ContextMenuApi.closeContextMenu(),
textSelection: "",
favoriteableType: null,
favoriteableId: null,
favoriteableName: null,
itemHref: void 0,
itemSrc: void 0,
itemSafeSrc: void 0,
itemTextContent: void 0,
isFullSearchContextMenu: true
});
}
interface MessageActionsProps {
message: Message;
isFullSearchContextMenu?: boolean;
}
const contextMenuPatch: NavContextMenuPatchCallback = (children, props: MessageActionsProps) => {
if (props?.isFullSearchContextMenu == null) return;
const group = findGroupChildrenByChildId("devmode-copy-id", children, true);
group?.push(
CopyIdMenuItem({ id: props.message.author.id, label: i18n.Messages.COPY_ID_AUTHOR })
);
};
migratePluginSettings("FullSearchContext", "SearchReply");
export default definePlugin({
name: "FullSearchContext",
description: "Makes the message context menu in message search results have all options you'd expect",
authors: [Devs.Ven, Devs.Aria],
patches: [{
find: "onClick:this.handleMessageClick,",
replacement: {
match: /this(?=\.handleContextMenu\(\i,\i\))/,
replace: "$self"
}
}],
handleContextMenu(event: React.MouseEvent, message: Message) {
const channel = ChannelStore.getChannel(message.channel_id);
if (!channel) return;
event.stopPropagation();
ContextMenuApi.openContextMenu(event, contextMenuProps =>
<MessageMenu
message={message}
channel={channel}
onHeightUpdate={contextMenuProps.onHeightUpdate}
/>
);
},
contextMenus: {
"message-actions": contextMenuPatch
}
});

View file

@ -0,0 +1,13 @@
# IgnoreActivities
Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings.
![](https://github.com/user-attachments/assets/f0c19060-0ecf-4f1c-8165-a5aa40143c82)
![](https://github.com/user-attachments/assets/73c3fa7a-5b90-41ee-a4d6-91fa76458b74)
![](https://github.com/user-attachments/assets/1ab3fe73-3911-48d1-8a08-e976af614b41)
The activity stays showing as a detected game even if ignored, differently from the stock Toggle Detection button from Discord:
![](https://github.com/user-attachments/assets/08ea60c3-3a31-42de-ae4c-7535fbf1b45a)

View file

@ -26,6 +26,11 @@ interface IgnoredActivity {
type: ActivitiesTypes;
}
const enum FilterMode {
Whitelist,
Blacklist
}
const RunningGameStore = findStoreLazy("RunningGameStore");
const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!;
@ -70,14 +75,17 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity);
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
// Trigger activities recalculation
recalculateActivities();
}
function recalculateActivities() {
ShowCurrentGame.updateSetting(old => old);
}
function ImportCustomRPCComponent() {
return (
<Flex flexDirection="column">
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the allowed list</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the filter list</Forms.FormText>
<div>
<Button
onClick={() => {
@ -86,7 +94,7 @@ function ImportCustomRPCComponent() {
return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
}
const isAlreadyAdded = allowedIdsPushID?.(id);
const isAlreadyAdded = idsListPushID?.(id);
if (isAlreadyAdded) {
showToast("CustomRPC application ID is already added.", Toasts.Type.FAILURE);
}
@ -99,39 +107,39 @@ function ImportCustomRPCComponent() {
);
}
let allowedIdsPushID: ((id: string) => boolean) | null = null;
let idsListPushID: ((id: string) => boolean) | null = null;
function AllowedIdsComponent(props: { setValue: (value: string) => void; }) {
const [allowedIds, setAllowedIds] = useState<string>(settings.store.allowedIds ?? "");
function IdsListComponent(props: { setValue: (value: string) => void; }) {
const [idsList, setIdsList] = useState<string>(settings.store.idsList ?? "");
allowedIdsPushID = (id: string) => {
const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean));
idsListPushID = (id: string) => {
const currentIds = new Set(idsList.split(",").map(id => id.trim()).filter(Boolean));
const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);
const ids = Array.from(currentIds).join(", ");
setAllowedIds(ids);
setIdsList(ids);
props.setValue(ids);
return isAlreadyAdded;
};
useEffect(() => () => {
allowedIdsPushID = null;
idsListPushID = null;
}, []);
function handleChange(newValue: string) {
setAllowedIds(newValue);
setIdsList(newValue);
props.setValue(newValue);
}
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Allowed List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC)</Forms.FormText>
<Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText>
<TextInput
type="text"
value={allowedIds}
value={idsList}
onChange={handleChange}
placeholder="235834946571337729, 343383572805058560"
/>
@ -145,40 +153,62 @@ const settings = definePluginSettings({
description: "",
component: () => <ImportCustomRPCComponent />
},
allowedIds: {
listMode: {
type: OptionType.SELECT,
description: "Change the mode of the filter list",
options: [
{
label: "Whitelist",
value: FilterMode.Whitelist,
default: true
},
{
label: "Blacklist",
value: FilterMode.Blacklist,
}
],
onChange: recalculateActivities
},
idsList: {
type: OptionType.COMPONENT,
description: "",
default: "",
onChange(newValue: string) {
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
settings.store.allowedIds = Array.from(ids).join(", ");
settings.store.idsList = Array.from(ids).join(", ");
recalculateActivities();
},
component: props => <AllowedIdsComponent setValue={props.setValue} />
component: props => <IdsListComponent setValue={props.setValue} />
},
ignorePlaying: {
type: OptionType.BOOLEAN,
description: "Ignore all playing activities (These are usually game and RPC activities)",
default: false
default: false,
onChange: recalculateActivities
},
ignoreStreaming: {
type: OptionType.BOOLEAN,
description: "Ignore all streaming activities",
default: false
default: false,
onChange: recalculateActivities
},
ignoreListening: {
type: OptionType.BOOLEAN,
description: "Ignore all listening activities (These are usually spotify activities)",
default: false
default: false,
onChange: recalculateActivities
},
ignoreWatching: {
type: OptionType.BOOLEAN,
description: "Ignore all watching activities",
default: false
default: false,
onChange: recalculateActivities
},
ignoreCompeting: {
type: OptionType.BOOLEAN,
description: "Ignore all competing activities (These are normally special game activities)",
default: false
default: false,
onChange: recalculateActivities
}
}).withPrivateSettings<{
ignoredActivities: IgnoredActivity[];
@ -189,8 +219,8 @@ function getIgnoredActivities() {
}
function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.allowedIds.includes(id)) {
return false;
if (id && settings.store.idsList.includes(id)) {
return settings.store.listMode === FilterMode.Blacklist;
}
switch (type) {
@ -206,15 +236,15 @@ function isActivityTypeIgnored(type: number, id?: string) {
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.",
authors: [Devs.Nuckyz, Devs.Kylie],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below",
dependencies: ["UserSettingsAPI"],
settings,
patches: [
{
find: '="LocalActivityStore",',
find: '"LocalActivityStore"',
replacement: [
{
match: /HANG_STATUS.+?(?=!\i\(\)\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
@ -223,7 +253,7 @@ export default definePlugin({
]
},
{
find: '="ActivityTrackingStore",',
find: '"ActivityTrackingStore"',
replacement: {
match: /getVisibleRunningGames\(\).+?;(?=for)(?<=(\i)=\i\.\i\.getVisibleRunningGames.+?)/,
replace: (m, runningGames) => `${m}${runningGames}=${runningGames}.filter(({id,name})=>$self.isActivityNotIgnored({type:0,application_id:id,name}));`
@ -236,6 +266,7 @@ export default definePlugin({
replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`
}
},
// Discord has 2 different components for activities. Currently, the last is the one being used
{
find: ".activityTitleText,variant",
replacement: {
@ -244,15 +275,21 @@ export default definePlugin({
},
},
{
find: ".activityCardDetails,children",
find: ".promotedLabelWrapperNonBanner,children",
replacement: {
match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
match: /\.appDetailsHeaderContainer.+?children:\i.*?}\),(?<=application:(\i).+?)/,
replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
}
}
],
async start() {
// Migrate allowedIds
if (Settings.plugins.IgnoreActivities.allowedIds) {
settings.store.idsList = Settings.plugins.IgnoreActivities.allowedIds;
delete Settings.plugins.IgnoreActivities.allowedIds; // Remove allowedIds
}
const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
if (oldIgnoredActivitiesData != null) {
@ -279,7 +316,7 @@ export default definePlugin({
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
if (props.application_id != null) {
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id);
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
} else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) {

View file

@ -156,14 +156,10 @@ export default definePlugin({
patches: [
{
find: "Messages.OPEN_IN_BROWSER",
find: ".contain,SCALE_DOWN:",
replacement: {
// there are 2 image thingies. one for carosuel and one for the single image.
// so thats why i added global flag.
// also idk if this patch is good, should it be more specific?
// https://regex101.com/r/xfvNvV/1
match: /return.{1,200}\.wrapper.{1,200}src:\i,/g,
replace: `$&id: '${ELEMENT_ID}',`
match: /\.slide,\i\),/g,
replace: `$&id:"${ELEMENT_ID}",`
}
},
@ -183,15 +179,13 @@ export default definePlugin({
{
match: /componentWillUnmount\(\){/,
replace: "$&$self.unMountMagnifier();"
},
{
match: /componentDidUpdate\(\i\){/,
replace: "$&$self.updateMagnifier(this);"
}
]
},
{
find: ".carouselModal",
replacement: {
match: /(?<=\.carouselModal.{0,100}onClick:)\i,/,
replace: "()=>{},"
}
}
],
@ -226,6 +220,11 @@ export default definePlugin({
}
},
updateMagnifier(instance) {
this.unMountMagnifier();
this.renderMagnifier(instance);
},
unMountMagnifier() {
this.root?.unmount();
this.currentMagnifierElement = null;

View file

@ -23,12 +23,3 @@
/* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */
}
/* make the carousel take up less space so we can click the backdrop and exit out of it */
[class*="modalCarouselWrapper_"] {
top: 0 !important;
}
[class*="carouselModal_"] {
height: 0 !important;
}

View file

@ -66,14 +66,14 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
patch.replacement = [patch.replacement];
}
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
if (IS_REPORTER) {
patch.replacement.forEach(r => {
delete r.predicate;
});
}
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
patches.push(patch);
}
@ -105,6 +105,11 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) {
settings[d].enabled = true;
dep.isDependency = true;
});
if (p.commands?.length) {
Plugins.CommandsAPI.isDependency = true;
settings.CommandsAPI.enabled = true;
}
}
for (const p of pluginsValues) {

View file

@ -19,7 +19,7 @@
import * as DataStore from "@api/DataStore";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { ChannelStore, NavigationRouter, SelectedChannelStore, SelectedGuildStore } from "@webpack/common";
import { ChannelRouter, SelectedChannelStore, SelectedGuildStore } from "@webpack/common";
export interface LogoutEvent {
type: "LOGOUT";
@ -40,11 +40,6 @@ interface PreviousChannel {
let isSwitchingAccount = false;
let previousCache: PreviousChannel | undefined;
function attemptToNavigateToChannel(guildId: string | null, channelId: string) {
if (!ChannelStore.hasChannel(channelId)) return;
NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${channelId}`);
}
export default definePlugin({
name: "KeepCurrentChannel",
description: "Attempt to navigate to the channel you were in before switching accounts or loading Discord.",
@ -59,8 +54,9 @@ export default definePlugin({
if (!isSwitchingAccount) return;
isSwitchingAccount = false;
if (previousCache?.channelId)
attemptToNavigateToChannel(previousCache.guildId, previousCache.channelId);
if (previousCache?.channelId) {
ChannelRouter.transitionToChannel(previousCache.channelId);
}
},
async CHANNEL_SELECT({ guildId, channelId }: ChannelSelectEvent) {
@ -84,7 +80,7 @@ export default definePlugin({
await DataStore.set("KeepCurrentChannel_previousData", previousCache);
} else if (previousCache.channelId) {
attemptToNavigateToChannel(previousCache.guildId, previousCache.channelId);
ChannelRouter.transitionToChannel(previousCache.channelId);
}
}
});

View file

@ -5,15 +5,16 @@
*/
import { getCurrentChannel } from "@utils/discord";
import { isObjectEmpty } from "@utils/misc";
import { SelectedChannelStore, Tooltip, useEffect, useStateFromStores } from "@webpack/common";
import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat } from ".";
import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, ThreadMemberListStore } from ".";
import { OnlineMemberCountStore } from "./OnlineMemberCountStore";
export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) {
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const guildId = isTooltip ? tooltipGuildId! : currentChannel.guild_id;
const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id;
const totalCount = useStateFromStores(
[GuildMemberCountStore],
@ -30,10 +31,19 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
() => ChannelMemberStore.getProps(guildId, currentChannel?.id)
);
const threadGroups = useStateFromStores(
[ThreadMemberListStore],
() => ThreadMemberListStore.getMemberListSections(currentChannel?.id)
);
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {
onlineCount = groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0);
}
if (!isTooltip && threadGroups && !isObjectEmpty(threadGroups)) {
onlineCount = Object.values(threadGroups).reduce((total, curr) => total + (curr.sectionId === "offline" ? 0 : curr.userIds.length), 0);
}
useEffect(() => {
OnlineMemberCountStore.ensureCount(guildId);
}, [guildId]);

View file

@ -15,8 +15,8 @@ export const OnlineMemberCountStore = proxyLazy(() => {
const onlineMemberMap = new Map<string, number>();
class OnlineMemberCountStore extends Flux.Store {
getCount(guildId: string) {
return onlineMemberMap.get(guildId);
getCount(guildId?: string) {
return onlineMemberMap.get(guildId!);
}
async _ensureCount(guildId: string) {
@ -25,8 +25,8 @@ export const OnlineMemberCountStore = proxyLazy(() => {
await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id);
}
ensureCount(guildId: string) {
if (onlineMemberMap.has(guildId)) return;
ensureCount(guildId?: string) {
if (!guildId || onlineMemberMap.has(guildId)) return;
preloadQueue.push(() =>
this._ensureCount(guildId)

View file

@ -28,10 +28,14 @@ import { FluxStore } from "@webpack/types";
import { MemberCount } from "./MemberCount";
export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; };
export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId?: string): number | null; };
export 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; }[]; };
};
export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as FluxStore & {
getMemberListSections(channelId?: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; };
};
const settings = definePluginSettings({
toolTip: {

View file

@ -1,5 +1,6 @@
# MentionAvatars
Shows user avatars inside mentions
Shows user avatars and role icons inside mentions
![](https://github.com/user-attachments/assets/fc76ea47-5e19-4063-a592-c57785a75cc7)
![](https://github.com/user-attachments/assets/76c4c3d9-7cde-42db-ba84-903cbb40c163)

View file

@ -10,21 +10,42 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { SelectedGuildStore, useState } from "@webpack/common";
import { GuildStore, SelectedGuildStore, useState } from "@webpack/common";
import { User } from "discord-types/general";
const settings = definePluginSettings({
showAtSymbol: {
type: OptionType.BOOLEAN,
description: "Whether the the @ symbol should be displayed",
description: "Whether the the @ symbol should be displayed on user mentions",
default: true
}
});
function DefaultRoleIcon() {
return (
<svg
className="vc-mentionAvatars-icon vc-mentionAvatars-role-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M14 8.00598C14 10.211 12.206 12.006 10 12.006C7.795 12.006 6 10.211 6 8.00598C6 5.80098 7.794 4.00598 10 4.00598C12.206 4.00598 14 5.80098 14 8.00598ZM2 19.006C2 15.473 5.29 13.006 10 13.006C14.711 13.006 18 15.473 18 19.006V20.006H2V19.006Z"
/>
<path
d="M20.0001 20.006H22.0001V19.006C22.0001 16.4433 20.2697 14.4415 17.5213 13.5352C19.0621 14.9127 20.0001 16.8059 20.0001 19.006V20.006Z"
/>
<path
d="M14.8834 11.9077C16.6657 11.5044 18.0001 9.9077 18.0001 8.00598C18.0001 5.96916 16.4693 4.28218 14.4971 4.0367C15.4322 5.09511 16.0001 6.48524 16.0001 8.00598C16.0001 9.44888 15.4889 10.7742 14.6378 11.8102C14.7203 11.8418 14.8022 11.8743 14.8834 11.9077Z"
/>
</svg>
);
}
export default definePlugin({
name: "MentionAvatars",
description: "Shows user avatars inside mentions",
authors: [Devs.Ven],
description: "Shows user avatars and role icons inside mentions",
authors: [Devs.Ven, Devs.SerStars],
patches: [{
find: ".USER_MENTION)",
@ -32,6 +53,13 @@ export default definePlugin({
match: /children:"@"\.concat\((null!=\i\?\i:\i)\)(?<=\.useName\((\i)\).+?)/,
replace: "children:$self.renderUsername({username:$1,user:$2})"
}
},
{
find: ".ROLE_MENTION)",
replacement: {
match: /children:\[\i&&.{0,50}\.RoleDot.{0,300},\i(?=\])/,
replace: "$&,$self.renderRoleIcon(arguments[0])"
}
}],
settings,
@ -47,12 +75,31 @@ export default definePlugin({
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<img src={user.getAvatarURL(SelectedGuildStore.getGuildId(), 16, isHovering)} className="vc-mentionAvatars-avatar" />
<img
src={user.getAvatarURL(SelectedGuildStore.getGuildId(), 16, isHovering)}
className="vc-mentionAvatars-icon"
style={{ borderRadius: "50%" }}
/>
{getUsernameString(username)}
</span>
);
}, { noop: true })
}, { noop: true }),
renderRoleIcon: ErrorBoundary.wrap(({ roleId, guildId }: { roleId: string, guildId: string; }) => {
// Discord uses Role Mentions for uncached users because .... idk
if (!roleId) return null;
const role = GuildStore.getRole(guildId, roleId);
if (!role?.icon) return <DefaultRoleIcon />;
return (
<img
className="vc-mentionAvatars-icon vc-mentionAvatars-role-icon"
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
/>
);
}),
});
function getUsernameString(username: string) {

View file

@ -1,8 +1,16 @@
.vc-mentionAvatars-avatar {
.vc-mentionAvatars-icon {
vertical-align: middle;
width: 1em !important; /* insane discord sets width: 100% in channel topic */
height: 1em;
margin: 0 4px 0.2rem 2px;
border-radius: 50%;
box-sizing: border-box;
}
.vc-mentionAvatars-role-icon {
margin: 0 2px 0.2rem 4px;
}
/** don't display inside the ServerInfo modal owner mention */
.vc-gp-owner .vc-mentionAvatars-icon {
display: none;
}

View file

@ -74,7 +74,7 @@ export default definePlugin({
if (msg.deleted === true) return;
if (isMe) {
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id)) return;
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return;
MessageActions.startEditMessage(channel.id, msg.id, msg.content);
event.preventDefault();

View file

@ -82,7 +82,6 @@ export default definePlugin({
default: true
}
},
dependencies: ["CommandsAPI"],
async start() {
for (const tag of await getTags()) createTagCommand(tag);

View file

@ -33,7 +33,6 @@ export default definePlugin({
name: "MoreCommands",
description: "echo, lenny, mock",
authors: [Devs.Arjix, Devs.echo, Devs.Samu],
dependencies: ["CommandsAPI"],
commands: [
{
name: "echo",

View file

@ -24,7 +24,6 @@ export default definePlugin({
name: "MoreKaomoji",
description: "Adds more Kaomoji to discord. ヽ(´▽`)/",
authors: [Devs.JacobTm],
dependencies: ["CommandsAPI"],
commands: [
{ name: "dissatisfaction", description: " " },
{ name: "smug", description: "ಠ_ಠ" },

View file

@ -22,7 +22,7 @@ import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack";
import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip, useState } from "@webpack/common";
import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip } from "@webpack/common";
import type { Permissions, RC } from "@webpack/types";
import type { Channel, Guild, Message, User } from "discord-types/general";
@ -107,14 +107,8 @@ const defaultSettings = Object.fromEntries(
tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }])
) as TagSettings;
function SettingsComponent(props: { setValue(v: any): void; }) {
settings.store.tagSettings ??= defaultSettings;
const [tagSettings, setTagSettings] = useState(settings.store.tagSettings as TagSettings);
const setValue = (v: TagSettings) => {
setTagSettings(v);
props.setValue(v);
};
function SettingsComponent() {
const tagSettings = settings.store.tagSettings ??= defaultSettings;
return (
<Flex flexDirection="column">
@ -137,19 +131,13 @@ function SettingsComponent(props: { setValue(v: any): void; }) {
type="text"
value={tagSettings[t.name]?.text ?? t.displayName}
placeholder={`Text on tag (default: ${t.displayName})`}
onChange={v => {
tagSettings[t.name].text = v;
setValue(tagSettings);
}}
onChange={v => tagSettings[t.name].text = v}
className={Margins.bottom16}
/>
<Switch
value={tagSettings[t.name]?.showInChat ?? true}
onChange={v => {
tagSettings[t.name].showInChat = v;
setValue(tagSettings);
}}
onChange={v => tagSettings[t.name].showInChat = v}
hideBorder
>
Show in messages
@ -157,10 +145,7 @@ function SettingsComponent(props: { setValue(v: any): void; }) {
<Switch
value={tagSettings[t.name]?.showInNotChat ?? true}
onChange={v => {
tagSettings[t.name].showInNotChat = v;
setValue(tagSettings);
}}
onChange={v => tagSettings[t.name].showInNotChat = v}
hideBorder
>
Show in member list and profiles
@ -183,7 +168,7 @@ const settings = definePluginSettings({
tagSettings: {
type: OptionType.COMPONENT,
component: SettingsComponent,
description: "fill me",
description: "fill me"
}
});
@ -247,9 +232,9 @@ export default definePlugin({
}
},
{
find: 'copyMetaData:"User Tag"',
find: ".Messages.USER_PROFILE_PRONOUNS",
replacement: {
match: /(?=,botClass:)/,
match: /(?=,hideBotTag:!0)/,
replace: ",moreTags_channelId:arguments[0].moreTags_channelId"
}
},
@ -264,8 +249,8 @@ export default definePlugin({
match: /user:\i,nick:\i,/,
replace: "$&moreTags_channelId,"
}, {
match: /,botType:(\i),(?<=user:(\i).+?)/g,
replace: ",botType:$self.getTag({user:$2,channelId:moreTags_channelId,origType:$1,location:'not-chat'}),"
match: /,botType:(\i),botVerified:(\i),(?!discriminatorClass:)(?<=user:(\i).+?)/g,
replace: ",botType:$self.getTag({user:$3,channelId:moreTags_channelId,origType:$1,location:'not-chat'}),botVerified:$2,"
}
]
},

View file

@ -20,14 +20,15 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common";
import { Channel, User } from "discord-types/general";
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
const UserUtils = findByPropsLazy("getGlobalName");
const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds");
const ExpandableList = findComponentByCodeLazy('"PRESS_SECTION"');
const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon");
function getGroupDMName(channel: Channel) {
@ -39,50 +40,19 @@ function getGroupDMName(channel: Channel) {
.join(", ");
}
export default definePlugin({
name: "MutualGroupDMs",
description: "Shows mutual group dms in profiles",
authors: [Devs.amia],
const getMutualGroupDms = (userId: string) =>
ChannelStore.getSortedPrivateChannels()
.filter(c => c.isGroupDM() && c.recipients.includes(userId));
patches: [
{
find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded
replacement: {
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
replace: '$self.isBotOrSelf(arguments[0].user)?null:$1"MUTUAL_GDMS",children:$self.getMutualGDMCountText(arguments[0].user)}),'
}
},
{
find: ".USER_INFO_CONNECTIONS:case",
replacement: {
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs({user: $1, onClose: $2});"
}
},
{
find: ".MUTUAL_FRIENDS?(",
replacement: [
{
match: /(?<=onItemSelect:\i,children:)(\i)\.map/,
replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:$self.getMutualGDMCountText(arguments[0].user)}])].map"
},
{
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&"
}
]
}
],
const isBotOrSelf = (user: User) => user.bot || user.id === UserStore.getCurrentUser().id;
isBotOrSelf: (user: User) => user.bot || user.id === UserStore.getCurrentUser().id,
getMutualGDMCountText: (user: User) => {
const count = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).length;
function getMutualGDMCountText(user: User) {
const count = getMutualGroupDms(user.id).length;
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
},
}
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {
return mutualDms.map(c => (
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {
@ -102,6 +72,52 @@ export default definePlugin({
</div>
</Clickable>
));
}
const IS_PATCHED = Symbol("MutualGroupDMs.Patched");
export default definePlugin({
name: "MutualGroupDMs",
description: "Shows mutual group dms in profiles",
authors: [Devs.amia],
patches: [
{
find: ".MUTUAL_FRIENDS?(",
replacement: [
{
match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
replace: "$self.pushSection($1, arguments[0].user);$&"
},
{
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&"
}
]
},
{
find: 'section:"MUTUAL_FRIENDS"',
replacement: {
match: /\.openUserProfileModal.+?\)}\)}\)(?<=(\(0,\i\.jsxs?\)\(\i\.\i,{className:(\i)\.divider}\)).+?)/,
replace: "$&,$self.renderDMPageList({user: arguments[0].user, Divider: $1, listStyle: $2.list})"
}
}
],
pushSection(sections: any[], user: User) {
if (isBotOrSelf(user) || sections[IS_PATCHED]) return;
sections[IS_PATCHED] = true;
sections.push({
section: "MUTUAL_GDMS",
text: getMutualGDMCountText(user)
});
},
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const mutualGDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
const entries = renderClickableGDMs(mutualGDms, onClose);
return (
<ScrollerThin
@ -120,5 +136,23 @@ export default definePlugin({
}
</ScrollerThin>
);
}),
renderDMPageList: ErrorBoundary.wrap(({ user, Divider, listStyle }: { user: User, Divider: JSX.Element, listStyle: string; }) => {
const mutualGDms = getMutualGroupDms(user.id);
if (mutualGDms.length === 0) return null;
return (
<>
{Divider}
<ExpandableList
listClassName={listStyle}
header={"Mutual Groups"}
isLoading={false}
items={renderClickableGDMs(mutualGDms, () => { })}
/>
</>
);
})
});

View file

@ -25,6 +25,12 @@ import { Message } from "discord-types/general";
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
interface MessageDeleteProps {
collapsedReason: {
message: string;
};
}
export default definePlugin({
name: "NoBlockedMessages",
description: "Hides all blocked messages from chat completely.",
@ -35,13 +41,13 @@ export default definePlugin({
replacement: [
{
match: /let\{[^}]*collapsedReason[^}]*\}/,
replace: "return null;$&"
replace: "if($self.shouldHide(arguments[0]))return null;$&"
}
]
},
...[
'="MessageStore",',
'"displayName","ReadStateStore")'
'"MessageStore"',
'"ReadStateStore"'
].map(find => ({
find,
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,
@ -68,5 +74,9 @@ export default definePlugin({
} catch (e) {
new Logger("NoBlockedMessages").error("Failed to check if user is blocked:", e);
}
},
shouldHide(props: MessageDeleteProps) {
return !props?.collapsedReason?.message.includes("deleted");
}
});

View file

@ -1,5 +0,0 @@
# NoDefaultHangStatus
Disable the default hang status when joining voice channels
![Visualization](https://github.com/Vendicated/Vencord/assets/24937357/329a9742-236f-48f7-94ff-c3510eca505a)

View file

@ -0,0 +1,3 @@
# NoMaskedUrlPaste
Pasting a link while you have text selected will NOT paste your link as a masked link.

View file

@ -0,0 +1,23 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants.js";
import definePlugin from "@utils/types";
export default definePlugin({
name: "NoMaskedUrlPaste",
authors: [Devs.CatNoir],
description: "Pasting a link while having text selected will not paste as masked URL",
patches: [
{
find: ".selection,preventEmojiSurrogates:",
replacement: {
match: /if\(null!=\i.selection&&\i.\i.isExpanded\(\i.selection\)\)/,
replace: "if(false)"
}
}
],
});

View file

@ -62,16 +62,7 @@ export default definePlugin({
replace: "return 0;"
}
},
// New message requests hook
{
find: 'location:"use-message-requests-count"',
predicate: () => settings.store.hideMessageRequestsCount,
replacement: {
match: /getNonChannelAckId\(\i\.\i\.MESSAGE_REQUESTS\).+?return /,
replace: "$&0;"
}
},
// Old message requests hook
// Message requests hook
{
find: "getMessageRequestsCount(){",
predicate: () => settings.store.hideMessageRequestsCount,
@ -83,7 +74,7 @@ export default definePlugin({
// This prevents the Message Requests tab from always hiding due to the previous patch (and is compatible with spam requests)
// In short, only the red badge is hidden. Button visibility behavior isn't changed.
{
find: ".getSpamChannelsCount(),",
find: ".getSpamChannelsCount()",
predicate: () => settings.store.hideMessageRequestsCount,
replacement: {
match: /(?<=getSpamChannelsCount\(\),\i=)\i\.getMessageRequestsCount\(\)/,
@ -96,7 +87,7 @@ export default definePlugin({
replacement: {
// The two groups inside the first group grab the minified names of the variables,
// they are then referenced later to find unviewedTrialCount + unviewedDiscountCount.
match: /(?<=\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.{0,200}\i=)\1\+\2/,
match: /(?<=\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.{0,300}\i=)\1\+\2/,
replace: "0"
}
}

View file

@ -18,36 +18,21 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { UserStore } from "@webpack/common";
export default definePlugin({
name: "NoProfileThemes",
description: "Completely removes Nitro profile themes",
description: "Completely removes Nitro profile themes from everyone but yourself",
authors: [Devs.TheKodeToad],
patches: [
{
find: ".NITRO_BANNER,",
replacement: {
// = isPremiumAtLeast(user.premiumType, TIER_2)
match: /=(?=\i\.\i\.isPremiumAtLeast\(null==(\i))/,
// = user.banner && isPremiumAtLeast(user.premiumType, TIER_2)
replace: "=(arguments[0]?.bannerSrc||$1?.banner)&&"
}
},
{
find: ".avatarPositionPremiumNoBanner,default:",
replacement: {
// premiumUserWithoutBanner: foo().avatarPositionPremiumNoBanner, default: foo().avatarPositionNormal
match: /\.avatarPositionPremiumNoBanner(?=,default:\i\.(\i))/,
// premiumUserWithoutBanner: foo().avatarPositionNormal...
replace: ".$1"
}
},
{
find: "hasThemeColors(){",
replacement: {
match: /get canUsePremiumProfileCustomization\(\){return /,
replace: "$&false &&"
replace: "$&$self.isCurrentUser(this.userId)&&"
}
}
]
},
],
isCurrentUser: (userId: string) => userId === UserStore.getCurrentUser()?.id,
});

View file

@ -36,7 +36,7 @@ export default definePlugin({
}
],
shouldSkip(guildId: string, emoji: any) {
if (emoji.type !== "GUILD_EMOJI") {
if (emoji.type !== 1) {
return false;
}
if (settings.store.shownEmojis === "onlyUnicode") {

View file

@ -33,7 +33,7 @@ interface URLReplacementRule {
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant
const UrlReplacementRules: Record<string, URLReplacementRule> = {
spotify: {
match: /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/,
match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode|prerelease)\/(.+)(?:\?.+?)?$/,
replace: (_, type, id) => `spotify://${type}/${id}`,
description: "Open Spotify links in the Spotify app",
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/,
@ -91,14 +91,19 @@ export default definePlugin({
replace: "async function $1 if(await $self.handleLink(...arguments)) return;"
}
},
// Make Spotify profile activity links open in app on web
{
find: "WEB_OPEN(",
find: "no artist ids in metadata",
predicate: () => !IS_DISCORD_DESKTOP && pluginSettings.store.spotify,
replacement: {
match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g,
replace: "true$1VencordNative.native.openExternal"
replacement: [
{
match: /\i\.\i\.isProtocolRegistered\(\)/g,
replace: "true"
},
{
match: /!\(0,\i\.isDesktop\)\(\)/,
replace: "false"
}
]
},
{
find: ".CONNECTED_ACCOUNT_VIEWED,",

View file

@ -19,16 +19,11 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findLazy } from "@webpack";
import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
const InvitesDisabledExperiment = findLazy(m => m.definition?.id === "2022-07_invites_disabled");
function showDisableInvites(guildId: string) {
// Once the experiment is removed, this should keep working
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
// @ts-ignore
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
return !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
}
function disableInvites(guildId: string) {

View file

@ -21,8 +21,10 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { findByCodeLazy } from "@webpack";
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import { UnicodeEmoji } from "@webpack/types";
import type { Guild, Role, User } from "discord-types/general";
import { settings } from "..";
import { cl, getPermissionDescription, getPermissionString } from "../utils";
@ -42,15 +44,15 @@ export interface RoleOrUserPermission {
overwriteDeny?: bigint;
}
function openRolesAndUsersPermissionsModal(permissions: Array<RoleOrUserPermission>, guild: Guild, header: string) {
return openModal(modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
));
type GetRoleIconData = (role: Role, size: number) => { customIconSrc?: string; unicodeEmoji?: UnicodeEmoji; };
const getRoleIconData: GetRoleIconData = findByCodeLazy("convertSurrogateToName", "customIconSrc", "unicodeEmoji");
function getRoleIconSrc(role: Role) {
const icon = getRoleIconData(role, 20);
if (!icon) return;
const { customIconSrc, unicodeEmoji } = icon;
return customIconSrc ?? unicodeEmoji?.url;
}
function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; guild: Guild; modalProps: ModalProps; header: string; }) {
@ -86,31 +88,34 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
size={ModalSize.LARGE}
>
<ModalHeader>
<Text className={cl("perms-title")} variant="heading-lg/semibold">{header} permissions:</Text>
<Text className={cl("modal-title")} variant="heading-lg/semibold">{header} permissions:</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent>
<ModalContent className={cl("modal-content")}>
{!selectedItem && (
<div className={cl("perms-no-perms")}>
<div className={cl("modal-no-perms")}>
<Text variant="heading-lg/normal">No permissions to display!</Text>
</div>
)}
{selectedItem && (
<div className={cl("perms-container")}>
<div className={cl("perms-list")}>
<div className={cl("modal-container")}>
<ScrollerThin className={cl("modal-list")} orientation="auto">
{permissions.map((permission, index) => {
const user = UserStore.getUser(permission.id ?? "");
const role = roles[permission.id ?? ""];
const user: User | undefined = UserStore.getUser(permission.id ?? "");
const role: Role | undefined = roles[permission.id ?? ""];
const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined;
return (
<button
className={cl("perms-list-item-btn")}
<div
className={cl("modal-list-item-btn")}
onClick={() => selectItem(index)}
role="button"
tabIndex={0}
>
<div
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
className={cl("modal-list-item", { "modal-list-item-active": selectedItemIndex === index })}
onContextMenu={e => {
if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => (
@ -124,7 +129,6 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
ContextMenuApi.openContextMenu(e, () => (
<UserContextMenu
userId={permission.id!}
onClose={modalProps.onClose}
/>
));
}
@ -132,13 +136,19 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
>
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
<span
className={cl("perms-role-circle")}
className={cl("modal-role-circle")}
style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }}
/>
)}
{permission.type === PermissionType.User && user !== undefined && (
{permission.type === PermissionType.Role && roleIconSrc != null && (
<img
className={cl("perms-user-img")}
className={cl("modal-role-image")}
src={roleIconSrc}
/>
)}
{permission.type === PermissionType.User && user != null && (
<img
className={cl("modal-user-img")}
src={user.getAvatarURL(void 0, void 0, false)}
/>
)}
@ -147,28 +157,25 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
permission.type === PermissionType.Role
? role?.name ?? "Unknown Role"
: permission.type === PermissionType.User
? (user && getUniqueUsername(user)) ?? "Unknown User"
? (user != null && getUniqueUsername(user)) ?? "Unknown User"
: (
<Flex style={{ gap: "0.2em", justifyItems: "center" }}>
@owner
<OwnerCrownIcon
height={18}
width={18}
aria-hidden="true"
/>
<OwnerCrownIcon height={18} width={18} aria-hidden="true" />
</Flex>
)
}
</Text>
</div>
</button>
</div>
);
})}
</div>
<div className={cl("perms-perms")}>
</ScrollerThin>
<div className={cl("modal-divider")} />
<ScrollerThin className={cl("modal-perms")} orientation="auto">
{Object.entries(PermissionsBits).map(([permissionName, bit]) => (
<div className={cl("perms-perms-item")}>
<div className={cl("perms-perms-item-icon")}>
<div className={cl("modal-perms-item")}>
<div className={cl("modal-perms-item-icon")}>
{(() => {
const { permissions, overwriteAllow, overwriteDeny } = selectedItem;
@ -192,7 +199,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
</Tooltip>
</div>
))}
</div>
</ScrollerThin>
</div>
)}
</ModalContent>
@ -208,7 +215,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
aria-label="Role Options"
>
<Menu.MenuItem
id="vc-copy-role-id"
id={cl("copy-role-id")}
label={i18n.Messages.COPY_ID_ROLE}
action={() => {
Clipboard.copy(roleId);
@ -217,14 +224,13 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
{(settings.store as any).unsafeViewAsRole && (
<Menu.MenuItem
id="vc-pw-view-as-role"
id={cl("view-as-role")}
label={i18n.Messages.VIEW_AS_ROLE}
action={() => {
const role = GuildStore.getRole(guild.id, roleId);
if (!role) return;
onClose();
FluxDispatcher.dispatch({
type: "IMPERSONATE_UPDATE",
guildId: guild.id,
@ -235,15 +241,14 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
}
}
});
}
}
}}
/>
)}
</Menu.Menu>
);
}
function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
function UserContextMenu({ userId }: { userId: string; }) {
return (
<Menu.Menu
navId={cl("user-context-menu")}
@ -251,7 +256,7 @@ function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => v
aria-label="User Options"
>
<Menu.MenuItem
id="vc-copy-user-id"
id={cl("copy-user-id")}
label={i18n.Messages.COPY_ID_USER}
action={() => {
Clipboard.copy(userId);
@ -263,4 +268,13 @@ function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => v
const RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent);
export default openRolesAndUsersPermissionsModal;
export default function openRolesAndUsersPermissionsModal(permissions: Array<RoleOrUserPermission>, guild: Guild, header: string) {
return openModal(modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
));
}

View file

@ -29,22 +29,65 @@ import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermi
interface UserPermission {
permission: string;
roleName: string;
roleColor: string;
rolePosition: number;
}
type UserPermissions = Array<UserPermission>;
const Classes = proxyLazyWebpack(() =>
Object.assign({}, ...findBulk(
filters.byProps("roles", "rolePill", "rolePillBorder"),
filters.byProps("roleCircle", "dotBorderBase", "dotBorderColor"),
filters.byProps("roleNameOverflow", "root", "roleName", "roleRemoveButton")
))
) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>;
const { RoleRootClasses, RoleClasses, RoleBorderClasses } = proxyLazyWebpack(() => {
const [RoleRootClasses, RoleClasses, RoleBorderClasses] = findBulk(
filters.byProps("root", "expandButton", "collapseButton"),
filters.byProps("role", "roleCircle", "roleName"),
filters.byProps("roleCircle", "dot", "dotBorderColor")
) as Record<string, string>[];
function UserPermissionsComponent({ guild, guildMember, showBorder, forceOpen = false }: { guild: Guild; guildMember: GuildMember; showBorder: boolean; forceOpen?: boolean; }) {
const stns = settings.use(["permissionsSortOrder"]);
return { RoleRootClasses, RoleClasses, RoleBorderClasses };
});
interface FakeRoleProps extends React.HTMLAttributes<HTMLDivElement> {
text: string;
color: string;
}
function FakeRole({ text, color, ...props }: FakeRoleProps) {
return (
<div {...props} className={classes(RoleClasses.role)}>
<div className={RoleClasses.roleRemoveButton}>
<span
className={classes(RoleBorderClasses.roleCircle, RoleClasses.roleCircle)}
style={{ backgroundColor: color }}
/>
</div>
<div className={RoleClasses.roleName}>
<Text
className={RoleClasses.roleNameOverflow}
variant="text-xs/medium"
>
{text}
</Text>
</div>
</div>
);
}
interface GrantedByTooltipProps {
roleName: string;
roleColor: string;
}
function GrantedByTooltip({ roleName, roleColor }: GrantedByTooltipProps) {
return (
<>
<Text variant="text-sm/medium">Granted By</Text>
<FakeRole text={roleName} color={roleColor} />
</>
);
}
function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { guild: Guild; guildMember: GuildMember; forceOpen?: boolean; }) {
const { permissionsSortOrder } = settings.use(["permissionsSortOrder"]);
const [rolePermissions, userPermissions] = useMemo(() => {
const userPermissions: UserPermissions = [];
@ -65,6 +108,7 @@ function UserPermissionsComponent({ guild, guildMember, showBorder, forceOpen =
const OWNER = i18n.Messages.GUILD_OWNER || "Server Owner";
userPermissions.push({
permission: OWNER,
roleName: "Owner",
roleColor: "var(--primary-300)",
rolePosition: Infinity
});
@ -73,10 +117,11 @@ function UserPermissionsComponent({ guild, guildMember, showBorder, forceOpen =
sortUserRoles(userRoles);
for (const [permission, bit] of Object.entries(PermissionsBits)) {
for (const { permissions, colorString, position } of userRoles) {
for (const { permissions, colorString, position, name } of userRoles) {
if ((permissions & bit) === bit) {
userPermissions.push({
permission: getPermissionString(permission),
roleName: name,
roleColor: colorString || "var(--primary-300)",
rolePosition: position
});
@ -89,9 +134,7 @@ function UserPermissionsComponent({ guild, guildMember, showBorder, forceOpen =
userPermissions.sort((a, b) => b.rolePosition - a.rolePosition);
return [rolePermissions, userPermissions];
}, [stns.permissionsSortOrder]);
const { root, role, roleRemoveButton, roleNameOverflow, roles, rolePill, rolePillBorder, roleCircle, roleName } = Classes;
}, [permissionsSortOrder]);
return (
<ExpandableHeader
@ -108,46 +151,41 @@ function UserPermissionsComponent({ guild, guildMember, showBorder, forceOpen =
onDropDownClick={state => settings.store.defaultPermissionsDropdownState = !state}
defaultState={settings.store.defaultPermissionsDropdownState}
buttons={[
(<Tooltip text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
<Tooltip text={`Sorting by ${permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
{tooltipProps => (
<button
<div
{...tooltipProps}
className={cl("userperms-sortorder-btn")}
className={cl("user-sortorder-btn")}
role="button"
tabIndex={0}
onClick={() => {
stns.permissionsSortOrder = stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole;
settings.store.permissionsSortOrder = permissionsSortOrder === PermissionsSortOrder.HighestRole ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole;
}}
>
<svg
width="20"
height="20"
viewBox="0 96 960 960"
transform={stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
>
<path fill="var(--text-normal)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
</svg>
</button>
</div>
)}
</Tooltip>)
</Tooltip>
]}>
{userPermissions.length > 0 && (
<div className={classes(root, roles)}>
{userPermissions.map(({ permission, roleColor }) => (
<div className={classes(role, rolePill, showBorder ? rolePillBorder : null)}>
<div className={roleRemoveButton}>
<span
className={roleCircle}
style={{ backgroundColor: roleColor }}
/>
</div>
<div className={roleName}>
<Text
className={roleNameOverflow}
variant="text-xs/medium"
<div className={classes(RoleRootClasses.root)}>
{userPermissions.map(({ permission, roleColor, roleName }) => (
<Tooltip
text={<GrantedByTooltip roleName={roleName} roleColor={roleColor} />}
tooltipClassName={cl("granted-by-container")}
tooltipContentClassName={cl("granted-by-content")}
>
{permission}
</Text>
</div>
</div>
{tooltipProps => (
<FakeRole {...tooltipProps} text={permission} color={roleColor} />
)}
</Tooltip>
))}
</div>
)}

View file

@ -26,7 +26,7 @@ import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, Menu, PermissionsBits, Popout, TooltipContainer, UserStore } from "@webpack/common";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, UserStore } from "@webpack/common";
import type { Guild, GuildMember } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
@ -54,12 +54,12 @@ export const settings = definePluginSettings({
options: [
{ label: "Highest Role", value: PermissionsSortOrder.HighestRole, default: true },
{ label: "Lowest Role", value: PermissionsSortOrder.LowestRole }
],
]
},
defaultPermissionsDropdownState: {
description: "Whether the permissions dropdown on user popouts should be open by default",
type: OptionType.BOOLEAN,
default: false,
default: false
}
});
@ -73,14 +73,12 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
action={() => {
const guild = GuildStore.getGuild(guildId);
let permissions: RoleOrUserPermission[];
let header: string;
switch (type) {
case MenuItemParentType.User: {
const { permissions, header } = match(type)
.returnType<{ permissions: RoleOrUserPermission[], header: string; }>()
.with(MenuItemParentType.User, () => {
const member = GuildMemberStore.getMember(guildId, id!);
permissions = getSortedRoles(guild, member)
const permissions: RoleOrUserPermission[] = getSortedRoles(guild, member)
.map(role => ({
type: PermissionType.Role,
...role
@ -93,37 +91,37 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
});
}
header = member.nick ?? UserStore.getUser(member.userId).username;
break;
}
case MenuItemParentType.Channel: {
return {
permissions,
header: member.nick ?? UserStore.getUser(member.userId).username
};
})
.with(MenuItemParentType.Channel, () => {
const channel = ChannelStore.getChannel(id!);
permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({
const permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({
type: type as PermissionType,
id,
overwriteAllow: allow,
overwriteDeny: deny
})), guildId);
header = channel.name;
break;
}
default: {
permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
return {
permissions,
header: channel.name
};
})
.otherwise(() => {
const permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
type: PermissionType.Role,
...role
}));
header = guild.name;
break;
}
}
return {
permissions,
header: guild.name
};
});
openRolesAndUsersPermissionsModal(permissions, guild, header);
}}
@ -133,32 +131,34 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
return (children, props) => {
if (!props) return;
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild)))
if (
!props ||
(type === MenuItemParentType.User && !props.user) ||
(type === MenuItemParentType.Guild && !props.guild) ||
(type === MenuItemParentType.Channel && (!props.channel || !props.guild))
) {
return;
}
const group = findGroupChildrenByChildId(childId, children);
const item = (() => {
switch (type) {
case MenuItemParentType.User:
return MenuItem(props.guildId, props.user.id, type);
case MenuItemParentType.Channel:
return MenuItem(props.guild.id, props.channel.id, type);
case MenuItemParentType.Guild:
return MenuItem(props.guild.id);
default:
return null;
}
})();
const item = match(type)
.with(MenuItemParentType.User, () => MenuItem(props.guildId, props.user.id, type))
.with(MenuItemParentType.Channel, () => MenuItem(props.guild.id, props.channel.id, type))
.with(MenuItemParentType.Guild, () => MenuItem(props.guild.id))
.otherwise(() => null);
if (item == null) return;
if (group)
group.push(item);
else if (childId === "roles" && props.guildId)
if (group) {
return group.push(item);
}
// "roles" may not be present due to the member not having any roles. In that case, add it above "Copy ID"
if (childId === "roles" && props.guildId) {
children.splice(-1, 0, <Menu.MenuGroup>{item}</Menu.MenuGroup>);
}
};
}
@ -169,32 +169,22 @@ export default definePlugin({
settings,
patches: [
{
find: ".popularApplicationCommandIds,",
replacement: {
match: /showBorder:(.{0,60})}\),(?<=guild:(\i),guildMember:(\i),.+?)/,
replace: (m, showBoder, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember},${showBoder}),`
}
},
{
find: ".VIEW_ALL_ROLES,",
replacement: {
match: /children:"\+"\.concat\(\i\.length-\i\.length\).{0,20}\}\),/,
match: /\.expandButton,.+?null,/,
replace: "$&$self.ViewPermissionsButton(arguments[0]),"
}
}
],
UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBorder: boolean) =>
!!guildMember && <UserPermissions guild={guild} guildMember={guildMember} showBorder={showBorder} />,
ViewPermissionsButton: ErrorBoundary.wrap(({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) => (
<Popout
position="bottom"
align="center"
renderPopout={() => (
<Dialog className={PopoutClasses.container} style={{ width: "500px" }}>
<UserPermissions guild={guild} guildMember={guildMember} showBorder forceOpen />
<UserPermissions guild={guild} guildMember={guildMember} forceOpen />
</Dialog>
)}
>

View file

@ -1,20 +1,6 @@
/* User Permissions Component */
.vc-permviewer-userperms-title-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
margin-bottom: 6px;
}
.vc-permviewer-userperms-btns-container {
display: flex;
align-items: center;
}
.vc-permviewer-userperms-sortorder-btn {
all: unset;
.vc-permviewer-user-sortorder-btn {
cursor: pointer;
display: flex;
align-items: center;
@ -23,27 +9,17 @@
height: 24px;
}
.vc-permviewer-userperms-permdetails-btn {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
}
.vc-permviewer-userperms-toggleperms-btn {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
}
/* RolesAndUsersPermissions Component */
.vc-permviewer-perms-title {
.vc-permviewer-modal-content {
padding: 16px 4px 16px 16px;
}
.vc-permviewer-modal-title {
flex-grow: 1;
}
.vc-permviewer-perms-no-perms {
.vc-permviewer-modal-no-perms {
width: 100%;
height: 100%;
display: flex;
@ -52,101 +28,103 @@
text-align: center;
}
.vc-permviewer-perms-container {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-areas: "list permissions";
padding: 16px 0;
.vc-permviewer-modal-container {
width: 100%;
height: 100%;
display: flex;
gap: 8px;
}
.vc-permviewer-perms-list {
grid-area: list;
.vc-permviewer-modal-list {
display: flex;
flex-direction: column;
gap: 2px;
border-right: 2px solid var(--background-modifier-active);
padding-right: 8px;
width: 200px;
}
.vc-permviewer-perms-list-item-btn {
all: unset;
.vc-permviewer-modal-list-item-btn {
cursor: pointer;
}
.vc-permviewer-perms-list-item {
.vc-permviewer-modal-list-item {
display: flex;
align-items: center;
padding: 8px 5px;
cursor: pointer;
width: 230px;
gap: 8px;
padding: 8px;
border-radius: 5px;
}
.vc-permviewer-perms-list-item:hover {
.vc-permviewer-modal-list-item:hover {
background-color: var(--background-modifier-hover);
}
.vc-permviewer-perms-list-item-active {
.vc-permviewer-modal-list-item-active {
background-color: var(--background-modifier-selected);
}
.vc-permviewer-perms-list-item > div {
.vc-permviewer-modal-list-item > div {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.vc-permviewer-perms-role-circle {
.vc-permviewer-modal-role-circle {
border-radius: 50%;
width: 12px;
height: 12px;
margin-left: 3px;
margin-right: 11px;
flex-shrink: 0;
}
.vc-permviewer-perms-user-img {
.vc-permviewer-modal-role-image {
width: 20px;
height: 20px;
object-fit: contain;
}
.vc-permviewer-modal-user-img {
border-radius: 50%;
width: 20px;
height: 20px;
margin-right: 6px;
}
.vc-permviewer-perms-perms {
grid-area: permissions;
.vc-permviewer-modal-divider {
width: 2px;
background-color: var(--background-modifier-active);
}
.vc-permviewer-modal-perms {
display: flex;
flex-direction: column;
margin-left: 5px;
padding-right: 8px;
}
.vc-permviewer-perms-perms-item {
position: relative;
.vc-permviewer-modal-perms-item {
display: flex;
align-items: center;
padding: 10px;
gap: 5px;
padding: 10px 2px 10px 10px;
border-bottom: 2px solid var(--background-modifier-active);
}
.vc-permviewer-perms-perms-item:last-child {
.vc-permviewer-modal-perms-item:last-child {
border: 0;
}
.vc-permviewer-perms-perms-item-icon {
.vc-permviewer-modal-perms-item-icon {
border: 1px solid var(--background-modifier-selected);
width: 24px;
height: 24px;
margin-right: 5px;
}
.vc-permviewer-perms-perms-item .vc-info-icon {
.vc-permviewer-modal-perms-item .vc-info-icon {
color: var(--interactive-muted);
margin-left: auto;
cursor: pointer;
position: absolute;
right: 0;
scale: 0.9;
transition: color ease-in 0.1s;
}
.vc-permviewer-perms-perms-item .vc-info-icon:hover {
.vc-permviewer-modal-perms-item .vc-info-icon:hover {
color: var(--interactive-active);
}
@ -167,3 +145,14 @@
background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6));
border-color: var(--profile-body-border-color)
}
.vc-permviewer-granted-by-container {
max-width: 300px;
width: auto;
}
.vc-permviewer-granted-by-content {
display: flex;
align-items: center;
gap: 4px;
}

View file

@ -88,7 +88,6 @@ export default definePlugin({
name: "petpet",
description: "Adds a /petpet slash command to create headpet gifs from any image",
authors: [Devs.Ven],
dependencies: ["CommandsAPI"],
commands: [
{
inputType: ApplicationCommandInputType.BUILT_IN,

View file

@ -24,13 +24,13 @@ const settings = definePluginSettings({
export default definePlugin({
name: "PictureInPicture",
description: "Adds picture in picture to videos (next to the Download button)",
authors: [Devs.Nobody],
authors: [Devs.Lumap],
settings,
patches: [
{
find: ".nonMediaMosaicItem]",
find: ".removeMosaicItemHoverButton),",
replacement: {
match: /\.nonMediaMosaicItem\]:!(\i).{0,50}?children:\[(\S)/,
match: /\.nonMediaMosaicItem\]:!(\i).{0,50}?children:\[\S,(\S)/,
replace: "$&,$1&&$2&&$self.renderPiPButton(),"
},
},

Some files were not shown because too many files have changed in this diff Show more