Merge branch 'dev' into svg-embed

This commit is contained in:
Amia 2024-05-24 15:46:29 +02:00 committed by GitHub
commit 99cf950b95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
129 changed files with 3351 additions and 675 deletions

1
.npmrc
View file

@ -1 +1,2 @@
strict-peer-dependencies=false strict-peer-dependencies=false
package-manager-strict=false

View file

@ -1,11 +1,9 @@
{ {
"recommendations": [ "recommendations": [
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"eamodio.gitlens",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"ExodiusStudios.comment-anchors",
"formulahendry.auto-rename-tag",
"GregorBiswanger.json2ts", "GregorBiswanger.json2ts",
"stylelint.vscode-stylelint" "stylelint.vscode-stylelint",
"Vendicated.vencord-companion"
] ]
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.8.2", "version": "1.8.6",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -19,16 +19,17 @@
"scripts": { "scripts": {
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs", "build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch",
"generatePluginJson": "tsx scripts/generatePluginList.ts", "generatePluginJson": "tsx scripts/generatePluginList.ts",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "node scripts/runInstaller.mjs", "inject": "node scripts/runInstaller.mjs",
"uninject": "node scripts/runInstaller.mjs",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson", "test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit", "testTsc": "tsc --noEmit"
"uninject": "node scripts/runInstaller.mjs",
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch"
}, },
"dependencies": { "dependencies": {
"@sapphi-red/web-noise-suppressor": "0.3.3", "@sapphi-red/web-noise-suppressor": "0.3.3",
@ -65,11 +66,12 @@
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",
"stylelint": "^15.6.0", "stylelint": "^15.6.0",
"stylelint-config-standard": "^33.0.0", "stylelint-config-standard": "^33.0.0",
"ts-patch": "^3.1.2",
"tsx": "^3.12.7", "tsx": "^3.12.7",
"type-fest": "^3.9.0", "type-fest": "^3.9.0",
"typescript": "^5.0.4", "typescript": "^5.4.5",
"zip-local": "^0.3.5", "typescript-transform-paths": "^3.4.7",
"zustand": "^3.7.2" "zip-local": "^0.3.5"
}, },
"packageManager": "pnpm@9.1.0", "packageManager": "pnpm@9.1.0",
"pnpm": { "pnpm": {

7
packages/vencord-types/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
*
!.*ignore
!package.json
!*.md
!prepare.ts
!index.d.ts
!globals.d.ts

View file

@ -0,0 +1,4 @@
node_modules
prepare.ts
.gitignore
HOW2PUB.md

View file

@ -0,0 +1,5 @@
# How to publish
1. run `pnpm generateTypes` in the project root
2. bump package.json version
3. npm publish

View file

@ -0,0 +1,11 @@
# Vencord Types
Typings for Vencord's api, published to npm
```sh
npm i @vencord/types
yarn add @vencord/types
pnpm add @vencord/types
```

24
packages/vencord-types/globals.d.ts vendored Normal file
View file

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

5
packages/vencord-types/index.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/* eslint-disable */
/// <reference path="Vencord.d.ts" />
/// <reference path="globals.d.ts" />
/// <reference path="modules.d.ts" />

View file

@ -0,0 +1,28 @@
{
"name": "@vencord/types",
"private": false,
"version": "0.1.3",
"description": "",
"types": "index.d.ts",
"scripts": {
"prepublishOnly": "tsx ./prepare.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Vencord",
"license": "GPL-3.0",
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0",
"tsx": "^3.12.6"
},
"dependencies": {
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.0.10",
"discord-types": "^1.3.26",
"standalone-electron-types": "^1.0.0",
"type-fest": "^3.5.3"
}
}

View file

@ -0,0 +1,47 @@
/*
* 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 { cpSync, moveSync, readdirSync, rmSync } from "fs-extra";
import { join } from "path";
readdirSync(join(__dirname, "src"))
.forEach(child => moveSync(join(__dirname, "src", child), join(__dirname, child), { overwrite: true }));
const VencordSrc = join(__dirname, "..", "..", "src");
for (const file of ["preload.d.ts", "userplugins", "main", "debug", "src", "browser", "scripts"]) {
rmSync(join(__dirname, file), { recursive: true, force: true });
}
function copyDtsFiles(from: string, to: string) {
for (const file of readdirSync(from, { withFileTypes: true })) {
// bad
if (from === VencordSrc && file.name === "globals.d.ts") continue;
const fullFrom = join(from, file.name);
const fullTo = join(to, file.name);
if (file.isDirectory()) {
copyDtsFiles(fullFrom, fullTo);
} else if (file.name.endsWith(".d.ts")) {
cpSync(fullFrom, fullTo);
}
}
}
copyDtsFiles(VencordSrc, __dirname);

File diff suppressed because it is too large Load diff

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
packages:
- packages/*

View file

@ -243,19 +243,27 @@ page.on("console", async e => {
} }
} }
if (isDebug) { async function getText() {
console.error(e.text());
} else if (level === "error") {
const text = await Promise.all(
e.args().map(async a => {
try { try {
return await Promise.all(
e.args().map(async a => {
return await maybeGetError(a) || await a.jsonValue(); return await maybeGetError(a) || await a.jsonValue();
} catch (e) {
return a.toString();
}
}) })
).then(a => a.join(" ").trim()); ).then(a => a.join(" ").trim());
} catch {
return e.text();
}
}
if (isDebug) {
const text = await getText();
console.error(text);
if (text.includes("A fatal error occurred:")) {
process.exit(1);
}
} else if (level === "error") {
const text = await getText();
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) { if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
console.error("[Unexpected Error]", text); console.error("[Unexpected Error]", text);
@ -303,8 +311,10 @@ async function runtime(token: string) {
delete patch.predicate; delete patch.predicate;
delete patch.group; delete patch.group;
if (!Array.isArray(patch.replacement)) Vencord.Util.canonicalizeFind(patch);
if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement]; patch.replacement = [patch.replacement];
}
patch.replacement.forEach(r => { patch.replacement.forEach(r => {
delete r.predicate; delete r.predicate;
@ -320,22 +330,31 @@ async function runtime(token: string) {
const validChunks = new Set<string>(); const validChunks = new Set<string>();
const invalidChunks = new Set<string>(); const invalidChunks = new Set<string>();
const deferredRequires = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void; let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r); const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
// True if resolved, false otherwise // True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>; const chunksSearchPromises = [] as Array<() => boolean>;
const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g); const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
async function searchAndLoadLazyChunks(factoryCode: string) { async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(lazyChunkRegex); const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]); // the chunk containing the component
if (chunkIds.length === 0) return; const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => {
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
if (chunkIds.length === 0) {
return;
}
let invalidChunkGroup = false; let invalidChunkGroup = false;
@ -371,6 +390,11 @@ async function runtime(token: string) {
// Requires the entry points for all valid chunk groups // Requires the entry points for all valid chunk groups
for (const [, entryPoint] of validChunkGroups) { for (const [, entryPoint] of validChunkGroups) {
try { try {
if (shouldForceDefer) {
deferredRequires.add(entryPoint);
continue;
}
if (wreq.m[entryPoint]) wreq(entryPoint as any); if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -433,6 +457,11 @@ async function runtime(token: string) {
await chunksSearchingDone; await chunksSearchingDone;
// Require deferred entry points
for (const deferredRequire of deferredRequires) {
wreq!(deferredRequire as any);
}
// All chunks Discord has mapped to asset files, even if they are not used anymore // All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as string[]; const allChunks = [] as string[];
@ -512,7 +541,6 @@ async function runtime(token: string) {
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000); setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
} catch (e) { } catch (e) {
console.log("[PUP_DEBUG]", "A fatal error occurred:", e); console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
process.exit(1);
} }
} }

View file

@ -17,6 +17,7 @@
*/ */
export * as Api from "./api"; export * as Api from "./api";
export * as Components from "./components";
export * as Plugins from "./plugins"; export * as Plugins from "./plugins";
export * as Util from "./utils"; export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss"; export * as QuickCss from "./utils/quickCss";

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { mergeDefaults } from "@utils/misc"; import { mergeDefaults } from "@utils/mergeDefaults";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { MessageActions, SnowflakeUtils } from "@webpack/common"; import { MessageActions, SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";

View file

@ -100,6 +100,7 @@ export async function showNotification(data: NotificationData) {
const n = new Notification(title, { const n = new Notification(title, {
body, body,
icon, icon,
// @ts-expect-error ts is drunk
image image
}); });
n.onclick = onClick; n.onclick = onClick;

View file

@ -20,7 +20,7 @@ import { debounce } from "@shared/debounce";
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore"; import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
import { localStorage } from "@utils/localStorage"; import { localStorage } from "@utils/localStorage";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { mergeDefaults } from "@utils/misc"; import { mergeDefaults } from "@utils/mergeDefaults";
import { putCloudSettings } from "@utils/settingsSync"; import { putCloudSettings } from "@utils/settingsSync";
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types"; import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common"; import { React } from "@webpack/common";

View file

@ -16,10 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./ExpandableHeader.css";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Text, Tooltip, useState } from "@webpack/common"; import { Text, Tooltip, useState } from "@webpack/common";
export const cl = classNameFactory("vc-expandableheader-");
import "./ExpandableHeader.css"; const cl = classNameFactory("vc-expandableheader-");
export interface ExpandableHeaderProps { export interface ExpandableHeaderProps {
onMoreClick?: () => void; onMoreClick?: () => void;
@ -31,7 +33,7 @@ export interface ExpandableHeaderProps {
buttons?: React.ReactNode[]; buttons?: React.ReactNode[];
} }
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) { export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
const [showContent, setShowContent] = useState(defaultState); const [showContent, setShowContent] = useState(defaultState);
return ( return (

View file

@ -21,7 +21,7 @@ import "./addonCard.css";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Badge } from "@components/Badge"; import { Badge } from "@components/Badge";
import { Switch } from "@components/Switch"; import { Switch } from "@components/Switch";
import { Text } from "@webpack/common"; import { Text, useRef } from "@webpack/common";
import type { MouseEventHandler, ReactNode } from "react"; import type { MouseEventHandler, ReactNode } from "react";
const cl = classNameFactory("vc-addon-"); const cl = classNameFactory("vc-addon-");
@ -42,6 +42,8 @@ interface Props {
} }
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) { export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
const titleRef = useRef<HTMLDivElement>(null);
const titleContainerRef = useRef<HTMLDivElement>(null);
return ( return (
<div <div
className={cl("card", { "card-disabled": disabled })} className={cl("card", { "card-disabled": disabled })}
@ -51,7 +53,21 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
<div className={cl("header")}> <div className={cl("header")}>
<div className={cl("name-author")}> <div className={cl("name-author")}>
<Text variant="text-md/bold" className={cl("name")}> <Text variant="text-md/bold" className={cl("name")}>
{name}{isNew && <Badge text="NEW" color="#ED4245" />} <div ref={titleContainerRef} className={cl("title-container")}>
<div
ref={titleRef}
className={cl("title")}
onMouseOver={() => {
const title = titleRef.current!;
const titleContainer = titleContainerRef.current!;
title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`);
title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);
}}
>
{name}
</div>
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
</Text> </Text>
{!!author && ( {!!author && (
<Text variant="text-md/normal" className={cl("author")}> <Text variant="text-md/normal" className={cl("author")}>

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { CheckedTextInput } from "@components/CheckedTextInput";
import { CodeBlock } from "@components/CodeBlock"; import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@shared/debounce"; import { debounce } from "@shared/debounce";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
@ -47,7 +46,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
interface ReplacementComponentProps { interface ReplacementComponentProps {
module: [id: number, factory: Function]; module: [id: number, factory: Function];
match: string | RegExp; match: string;
replacement: string | ReplaceFn; replacement: string | ReplaceFn;
setReplacementError(error: any): void; setReplacementError(error: any): void;
} }
@ -58,7 +57,13 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
const [patchedCode, matchResult, diff] = React.useMemo(() => { const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", ""); const src: string = fact.toString().replaceAll("\n", "");
const canonicalMatch = canonicalizeMatch(match);
try {
new RegExp(match);
} catch (e) {
return ["", [], []];
}
const canonicalMatch = canonicalizeMatch(new RegExp(match));
try { try {
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin"); const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
var patched = src.replace(canonicalMatch, canonicalReplace as string); var patched = src.replace(canonicalMatch, canonicalReplace as string);
@ -180,7 +185,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
return ( return (
<> <>
<Forms.FormTitle>replacement</Forms.FormTitle> {/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
<Forms.FormTitle className="">replacement</Forms.FormTitle>
<TextInput <TextInput
value={replacement?.toString()} value={replacement?.toString()}
onChange={onChange} onChange={onChange}
@ -188,7 +194,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
/> />
{!isFunc && ( {!isFunc && (
<div className="vc-text-selectable"> <div className="vc-text-selectable">
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
{Object.entries({ {Object.entries({
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)", "\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
"$$": "Insert a $", "$$": "Insert a $",
@ -220,11 +226,12 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
interface FullPatchInputProps { interface FullPatchInputProps {
setFind(v: string): void; setFind(v: string): void;
setParsedFind(v: string | RegExp): void;
setMatch(v: string): void; setMatch(v: string): void;
setReplacement(v: string | ReplaceFn): void; setReplacement(v: string | ReplaceFn): void;
} }
function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputProps) { function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
const [fullPatch, setFullPatch] = React.useState<string>(""); const [fullPatch, setFullPatch] = React.useState<string>("");
const [fullPatchError, setFullPatchError] = React.useState<string>(""); const [fullPatchError, setFullPatchError] = React.useState<string>("");
@ -233,6 +240,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
setFullPatchError(""); setFullPatchError("");
setFind(""); setFind("");
setParsedFind("");
setMatch(""); setMatch("");
setReplacement(""); setReplacement("");
return; return;
@ -256,7 +264,8 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field"); if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field"); if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
setFind(parsed.find); setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
setParsedFind(parsed.find);
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match); setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
setReplacement(parsed.replacement.replace); setReplacement(parsed.replacement.replace);
setFullPatchError(""); setFullPatchError("");
@ -266,7 +275,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
} }
return <> return <>
<Forms.FormText>Paste your full JSON patch here to fill out the fields</Forms.FormText> <Forms.FormText className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Forms.FormText>
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} /> <TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>} {fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
</>; </>;
@ -274,6 +283,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
function PatchHelper() { function PatchHelper() {
const [find, setFind] = React.useState<string>(""); const [find, setFind] = React.useState<string>("");
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
const [match, setMatch] = React.useState<string>(""); const [match, setMatch] = React.useState<string>("");
const [replacement, setReplacement] = React.useState<string | ReplaceFn>(""); const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
@ -281,34 +291,46 @@ function PatchHelper() {
const [module, setModule] = React.useState<[number, Function]>(); const [module, setModule] = React.useState<[number, Function]>();
const [findError, setFindError] = React.useState<string>(); const [findError, setFindError] = React.useState<string>();
const [matchError, setMatchError] = React.useState<string>();
const code = React.useMemo(() => { const code = React.useMemo(() => {
return ` return `
{ {
find: ${JSON.stringify(find)}, find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
replacement: { replacement: {
match: /${match.replace(/(?<!\\)\//g, "\\/")}/, match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)} replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
} }
} }
`.trim(); `.trim();
}, [find, match, replacement]); }, [parsedFind, match, replacement]);
function onFindChange(v: string) { function onFindChange(v: string) {
setFindError(void 0);
setFind(v); setFind(v);
try {
let parsedFind = v as string | RegExp;
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
setFindError(void 0);
setParsedFind(parsedFind);
if (v.length) { if (v.length) {
findCandidates({ find: v, setModule, setError: setFindError }); findCandidates({ find: parsedFind, setModule, setError: setFindError });
}
} catch (e: any) {
setFindError((e as Error).message);
} }
} }
function onMatchChange(v: string) { function onMatchChange(v: string) {
setMatch(v);
try { try {
new RegExp(v); new RegExp(v);
setFindError(void 0); setMatchError(void 0);
setMatch(v);
} catch (e: any) { } catch (e: any) {
setFindError((e as Error).message); setMatchError((e as Error).message);
} }
} }
@ -317,11 +339,12 @@ function PatchHelper() {
<Forms.FormTitle>full patch</Forms.FormTitle> <Forms.FormTitle>full patch</Forms.FormTitle>
<FullPatchInput <FullPatchInput
setFind={onFindChange} setFind={onFindChange}
setParsedFind={setParsedFind}
setMatch={onMatchChange} setMatch={onMatchChange}
setReplacement={setReplacement} setReplacement={setReplacement}
/> />
<Forms.FormTitle>find</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
<TextInput <TextInput
type="text" type="text"
value={find} value={find}
@ -329,19 +352,15 @@ function PatchHelper() {
error={findError} error={findError}
/> />
<Forms.FormTitle>match</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
<CheckedTextInput <TextInput
type="text"
value={match} value={match}
onChange={onMatchChange} onChange={onMatchChange}
validate={v => { error={matchError}
try {
return (new RegExp(v), true);
} catch (e) {
return (e as Error).message;
}
}}
/> />
<div className={Margins.top8} />
<ReplacementInput <ReplacementInput
replacement={replacement} replacement={replacement}
setReplacement={setReplacement} setReplacement={setReplacement}
@ -352,7 +371,7 @@ function PatchHelper() {
{module && ( {module && (
<ReplacementComponent <ReplacementComponent
module={module} module={module}
match={new RegExp(match)} match={match}
replacement={replacement} replacement={replacement}
setReplacementError={setReplacementError} setReplacementError={setReplacementError}
/> />

View file

@ -62,3 +62,36 @@
.vc-addon-author::before { .vc-addon-author::before {
content: "by "; content: "by ";
} }
.vc-addon-title-container {
width: 100%;
overflow: hidden;
height: 1.25em;
position: relative;
}
.vc-addon-title {
position: absolute;
inset: 0;
overflow: hidden;
text-overflow: ellipsis;
}
@keyframes vc-addon-title {
0% {
transform: translateX(0);
}
50% {
transform: translateX(var(--offset));
}
100% {
transform: translateX(0);
}
}
.vc-addon-title:hover {
overflow: visible;
animation: vc-addon-title var(--duration) linear infinite;
}

18
src/components/index.ts Normal file
View file

@ -0,0 +1,18 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "./Badge";
export * from "./CheckedTextInput";
export * from "./CodeBlock";
export * from "./DonateButton";
export { default as ErrorBoundary } from "./ErrorBoundary";
export * from "./ErrorCard";
export * from "./ExpandableHeader";
export * from "./Flex";
export * from "./Heart";
export * from "./Icons";
export * from "./Link";
export * from "./Switch";

View file

@ -73,6 +73,9 @@ if (!IS_VANILLA) {
const original = options.webPreferences.preload; const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js"); options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
options.webPreferences.sandbox = false; options.webPreferences.sandbox = false;
// work around discord unloading when in background
options.webPreferences.backgroundThrottling = false;
if (settings.frameless) { if (settings.frameless) {
options.frame = false; options.frame = false;
} else if (process.platform === "win32" && settings.winNativeTitleBar) { } else if (process.platform === "win32" && settings.winNativeTitleBar) {
@ -136,6 +139,9 @@ if (!IS_VANILLA) {
} }
return originalAppend.apply(this, args); return originalAppend.apply(this, args);
}; };
// Work around discord unloading when in background
app.commandLine.appendSwitch("disable-renderer-backgrounding");
} else { } else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord"); console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
} }

View file

@ -7,6 +7,7 @@
import type { Settings } from "@api/Settings"; import type { Settings } from "@api/Settings";
import { IpcEvents } from "@shared/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
import { SettingsStore } from "@shared/SettingsStore"; import { SettingsStore } from "@shared/SettingsStore";
import { mergeDefaults } from "@utils/mergeDefaults";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { mkdirSync, readFileSync, writeFileSync } from "fs"; import { mkdirSync, readFileSync, writeFileSync } from "fs";
@ -42,7 +43,22 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string
RendererSettings.setData(data, pathToNotify); RendererSettings.setData(data, pathToNotify);
}); });
export const NativeSettings = new SettingsStore(readSettings("native", NATIVE_SETTINGS_FILE)); export interface NativeSettings {
plugins: {
[plugin: string]: {
[setting: string]: any;
};
};
}
const DefaultNativeSettings: NativeSettings = {
plugins: {}
};
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
mergeDefaults(nativeSettings, DefaultNativeSettings);
export const NativeSettings = new SettingsStore(nativeSettings);
NativeSettings.addGlobalChangeListener(() => { NativeSettings.addGlobalChangeListener(() => {
try { try {

View file

@ -35,6 +35,7 @@ export const ALLOWED_PROTOCOLS = [
"steam:", "steam:",
"spotify:", "spotify:",
"com.epicgames.launcher:", "com.epicgames.launcher:",
"tidal:"
]; ];
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla"); export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");

2
src/modules.d.ts vendored
View file

@ -20,7 +20,7 @@
/// <reference types="standalone-electron-types"/> /// <reference types="standalone-electron-types"/>
declare module "~plugins" { declare module "~plugins" {
const plugins: Record<string, import("@utils/types").Plugin>; const plugins: Record<string, import("./utils/types").Plugin>;
export default plugins; export default plugins;
} }

View file

@ -29,7 +29,7 @@ export default definePlugin({
find: '"NoticeStore"', find: '"NoticeStore"',
replacement: [ replacement: [
{ {
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g, match: /(?<=!1;)\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&" replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
}, },
{ {

View file

@ -26,28 +26,39 @@ import UpdaterTab from "@components/VencordSettings/UpdaterTab";
import VencordTab from "@components/VencordSettings/VencordTab"; import VencordTab from "@components/VencordSettings/VencordTab";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common"; import { i18n, React } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
type SectionType = "HEADER" | "DIVIDER" | "CUSTOM";
type SectionTypes = Record<SectionType, SectionType>;
export default definePlugin({ export default definePlugin({
name: "Settings", name: "Settings",
description: "Adds Settings UI and debug info", description: "Adds Settings UI and debug info",
authors: [Devs.Ven, Devs.Megu], authors: [Devs.Ven, Devs.Megu],
required: true, required: true,
patches: [{ patches: [
{
find: ".versionHash", find: ".versionHash",
replacement: [ replacement: [
{ {
match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/, match: /\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
replace: (m, component, props) => { replace: (m, component, props) => {
props = props.replace(/children:\[.+\]/, ""); props = props.replace(/children:\[.+\]/, "");
return `${m},$self.makeInfoElements(${component}, ${props})`; return `${m},$self.makeInfoElements(${component}, ${props})`;
} }
},
{
match: /copyValue:\i\.join\(" "\)/,
replace: "$& + $self.getInfoString()"
} }
] ]
}, { },
// Discord Stable
// FIXME: remove once change merged to stable
{
find: "Messages.ACTIVITY_SETTINGS", find: "Messages.ACTIVITY_SETTINGS",
replacement: { replacement: {
get match() { get match() {
@ -64,17 +75,33 @@ export default definePlugin({
}, },
replace: "...$self.makeSettingsCategories($1),$&" replace: "...$self.makeSettingsCategories($1),$&"
} }
}, { },
{
find: "Messages.ACTIVITY_SETTINGS",
replacement: {
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}
},
{
find: "useDefaultUserSettingsSections:function",
replacement: {
match: /(?<=useDefaultUserSettingsSections:function\(\){return )(\i)\}/,
replace: "$self.wrapSettingsHook($1)}"
}
},
{
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL", find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: { replacement: {
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/, match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
replace: "$2.default.open($1);return;" replace: "$2.default.open($1);return;"
} }
}], }
],
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[], customSections: [] as ((SectionTypes: SectionTypes) => any)[],
makeSettingsCategories(SectionTypes: Record<string, unknown>) { makeSettingsCategories(SectionTypes: SectionTypes) {
return [ return [
{ {
section: SectionTypes.HEADER, section: SectionTypes.HEADER,
@ -130,19 +157,63 @@ export default definePlugin({
].filter(Boolean); ].filter(Boolean);
}, },
isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
const firstChild = settings?.[0];
// lowest two elements... sanity backup
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
const { settingsLocation } = Settings.plugins.Settings;
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
if (!header) return;
const names = {
top: i18n.Messages.USER_SETTINGS,
aboveNitro: i18n.Messages.BILLING_SETTINGS,
belowNitro: i18n.Messages.APP_SETTINGS,
aboveActivity: i18n.Messages.ACTIVITY_SETTINGS
};
return header === names[settingsLocation];
},
patchedSettings: new WeakSet(),
addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: SectionTypes) {
if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return;
this.patchedSettings.add(elements);
elements.push(...this.makeSettingsCategories(sectionTypes));
},
wrapSettingsHook(originalHook: (...args: any[]) => Record<string, unknown>[]) {
return (...args: any[]) => {
const elements = originalHook(...args);
if (!this.patchedSettings.has(elements))
elements.unshift(...this.makeSettingsCategories({
HEADER: "HEADER",
DIVIDER: "DIVIDER",
CUSTOM: "CUSTOM"
}));
return elements;
};
},
options: { options: {
settingsLocation: { settingsLocation: {
type: OptionType.SELECT, type: OptionType.SELECT,
description: "Where to put the Vencord settings section", description: "Where to put the Vencord settings section",
options: [ options: [
{ label: "At the very top", value: "top" }, { label: "At the very top", value: "top" },
{ label: "Above the Nitro section", value: "aboveNitro" }, { label: "Above the Nitro section", value: "aboveNitro", default: true },
{ label: "Below the Nitro section", value: "belowNitro" }, { label: "Below the Nitro section", value: "belowNitro" },
{ label: "Above Activity Settings", value: "aboveActivity", default: true }, { label: "Above Activity Settings", value: "aboveActivity" },
{ label: "Below Activity Settings", value: "belowActivity" }, { label: "Below Activity Settings", value: "belowActivity" },
{ label: "At the very bottom", value: "bottom" }, { label: "At the very bottom", value: "bottom" },
], ]
restartNeeded: true
}, },
}, },
@ -169,15 +240,24 @@ export default definePlugin({
return ""; return "";
}, },
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) { getInfoRows() {
const { electronVersion, chromiumVersion, additionalInfo } = this; const { electronVersion, chromiumVersion, additionalInfo } = this;
return ( const rows = [`Vencord ${gitHash}${additionalInfo}`];
<>
<Component {...props}>Vencord {gitHash}{additionalInfo}</Component> if (electronVersion) rows.push(`Electron ${electronVersion}`);
{electronVersion && <Component {...props}>Electron {electronVersion}</Component>} if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);
{chromiumVersion && <Component {...props}>Chromium {chromiumVersion}</Component>}
</> return rows;
},
getInfoString() {
return "\n" + this.getInfoRows().join("\n");
},
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
return this.getInfoRows().map((text, i) =>
<Component key={i} {...props}>{text}</Component>
); );
} }
}); });

View file

@ -139,7 +139,7 @@ ${makeCodeblock(enabledPlugins.join(", "))}
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles; const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return; if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
if (IS_UPDATER_DISABLED) { if (!IS_WEB && IS_UPDATER_DISABLED) {
return Alerts.show({ return Alerts.show({
title: "Hold on!", title: "Hold on!",
body: <div> body: <div>

View file

@ -31,10 +31,10 @@ export default definePlugin({
// Some modules match the find but the replacement is returned untouched // Some modules match the find but the replacement is returned untouched
noWarn: true, noWarn: true,
replacement: { replacement: {
match: /canAnimate:.+?(?=([,}].*?\)))/g, match: /canAnimate:.+?([,}].*?\))/g,
replace: (m, rest) => { replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/); const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) return "canAnimate:!0"; if (destructuringMatch == null) return `canAnimate:!0${rest}`;
return m; return m;
} }
} }

View file

@ -73,13 +73,13 @@ export default definePlugin({
{ {
find: "instantBatchUpload:function", find: "instantBatchUpload:function",
replacement: { replacement: {
match: /uploadFiles:(.{1,2}),/, match: /uploadFiles:(\i),/,
replace: replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),", "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
}, },
}, },
{ {
find: "message.attachments", find: 'addFilesTo:"message.attachments"',
replacement: { replacement: {
match: /(\i.uploadFiles\((\i),)/, match: /(\i.uploadFiles\((\i),)/,
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1" replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"

View file

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

@ -0,0 +1,73 @@
/*
* 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

@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, i18n } from "@webpack/common"; import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
import FolderSideBar from "./FolderSideBar"; import FolderSideBar from "./FolderSideBar";
@ -112,13 +112,13 @@ export default definePlugin({
replacement: [ replacement: [
// Create the isBetterFolders variable in the GuildsBar component // Create the isBetterFolders variable in the GuildsBar component
{ {
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/, match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/,
replace: ",isBetterFolders" replace: "$&,isBetterFolders"
}, },
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders // If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
{ {
match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/, match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},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 // If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{ {
@ -252,19 +252,21 @@ export default definePlugin({
} }
}, },
getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set<any>) { getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
if (!isBetterFolders || expandedFolderIds == null) return oldTree; return useMemo(() => {
if (!isBetterFolders || expandedFolderIds == null) return originalTree;
const newTree = new GuildsTree(); const newTree = new GuildsTree();
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders // Children is every folder and guild which is not in a folder, this filters out only the expanded folders
newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id)); newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them // Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
newTree.nodes = Object.fromEntries( newTree.nodes = Object.fromEntries(
Object.entries(oldTree.nodes) Object.entries(originalTree.nodes)
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId)) .filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
); );
return newTree; return newTree;
}, [isBetterFolders, originalTree, expandedFolderIds]);
}, },
makeGuildsBarGuildListFilter(isBetterFolders: boolean) { makeGuildsBarGuildListFilter(isBetterFolders: boolean) {

View file

@ -17,6 +17,7 @@
*/ */
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -60,7 +61,7 @@ export default definePlugin({
find: ".popularApplicationCommandIds,", find: ".popularApplicationCommandIds,",
replacement: { replacement: {
match: /lastSection:(!?\i)}\),/, match: /lastSection:(!?\i)}\),/,
replace: "$&$self.patchPadding($1)," replace: "$&$self.patchPadding({lastSection:$1}),"
} }
} }
], ],
@ -80,10 +81,10 @@ export default definePlugin({
} }
}, },
patchPadding(lastSection: any) { patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
if (!lastSection) return; if (!lastSection) return null;
return ( return (
<div className={UserPopoutSectionCssClasses.lastSection} ></div> <div className={UserPopoutSectionCssClasses.lastSection} ></div>
); );
} })
}); });

View file

@ -1,6 +1,6 @@
# BetterRoleContext # BetterRoleContext
Adds options to copy role color and edit role when right clicking roles in the user profile Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326) ![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190)

View file

@ -4,9 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { definePluginSettings } from "@api/Settings";
import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentGuild } from "@utils/discord"; import { getCurrentGuild, openImageModal } from "@utils/discord";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
@ -34,10 +36,34 @@ function AppearanceIcon() {
); );
} }
const settings = definePluginSettings({
roleIconFileFormat: {
type: OptionType.SELECT,
description: "File format to use when viewing role icons",
options: [
{
label: "png",
value: "png",
default: true
},
{
label: "webp",
value: "webp",
},
{
label: "jpg",
value: "jpg"
}
]
}
});
export default definePlugin({ export default definePlugin({
name: "BetterRoleContext", name: "BetterRoleContext",
description: "Adds options to copy role color / edit role when right clicking roles in the user profile", description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
authors: [Devs.Ven], authors: [Devs.Ven, Devs.goodbee],
settings,
start() { start() {
// DeveloperMode needs to be enabled for the context menu to be shown // DeveloperMode needs to be enabled for the context menu to be shown
@ -63,6 +89,20 @@ export default definePlugin({
); );
} }
if (role.icon) {
children.push(
<Menu.MenuItem
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}`);
}}
icon={ImageIcon}
/>
);
}
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) { if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push( children.push(
<Menu.MenuItem <Menu.MenuItem

View file

@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
import { React, RestAPI, Tooltip } from "@webpack/common"; import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
import { RenameButton } from "./components/RenameButton"; import { RenameButton } from "./components/RenameButton";
import { Session, SessionInfo } from "./types"; import { Session, SessionInfo } from "./types";
@ -168,7 +168,7 @@ export default definePlugin({
async checkNewSessions() { async checkNewSessions() {
const data = await RestAPI.get({ const data = await RestAPI.get({
url: "/auth/sessions" url: Constants.Endpoints.AUTH_SESSIONS
}); });
for (const session of data.body.user_sessions) { for (const session of data.body.user_sessions) {

View file

@ -6,17 +6,18 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { waitFor } from "@webpack";
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common"; import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
import type { HTMLAttributes, ReactElement } from "react"; import type { HTMLAttributes, ReactElement } from "react";
type SettingsEntry = { section: string, label: string; }; type SettingsEntry = { section: string, label: string; };
const cl = classNameFactory(""); const cl = classNameFactory("");
const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers"); let Classes: Record<string, string>;
waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
const settings = definePluginSettings({ const settings = definePluginSettings({
disableFade: { disableFade: {
@ -110,26 +111,33 @@ export default definePlugin({
{ // Load menu TOC eagerly { // Load menu TOC eagerly
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format", find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: { replacement: {
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/, match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
replace: "(async ()=>$2)()," replace: "$&(async ()=>$2)(),"
}, },
predicate: () => settings.store.eagerLoad predicate: () => settings.store.eagerLoad
}, },
{ // Settings cog context menu { // Settings cog context menu
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL", find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: { replacement: {
match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/, match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$self.wrapMenu($&)" replace: "$self.wrapMenu($&)"
} }
} }
], ],
// 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.
//
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
// not in children
Layer(props: LayerProps) { Layer(props: LayerProps) {
return ( if (!FocusLock || !ComponentDispatch || !Classes) {
<ErrorBoundary fallback={() => props.children as any}> new Logger("BetterSettings").error("Failed to find some components");
<Layer {...props} /> return props.children;
</ErrorBoundary> }
);
return <Layer {...props} />;
}, },
wrapMenu(list: SettingsEntry[]) { wrapMenu(list: SettingsEntry[]) {

View file

@ -34,9 +34,9 @@ export default definePlugin({
{ {
find: ".AVATAR_STATUS_MOBILE_16;", find: ".AVATAR_STATUS_MOBILE_16;",
replacement: { replacement: {
match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/, match: /(fromIsMobile:\i=!0,.+?)status:(\i)/,
// Rename field to force it to always use "online" // Rename field to force it to always use "online"
replace: 'status_$:$1="online"' replace: '$1status_$:$2="online"'
} }
} }
] ]

View file

@ -24,22 +24,20 @@ import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater"; import { maybePromptToUpdate } from "@utils/updater";
import { filters, findBulk, proxyLazyWebpack } from "@webpack"; import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common"; import { DraftType, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
const CrashHandlerLogger = new Logger("CrashHandler"); const CrashHandlerLogger = new Logger("CrashHandler");
const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
const modules = findBulk( const { ModalStack, DraftManager, closeExpressionPicker } = proxyLazyWebpack(() => {
const [ModalStack, DraftManager, ExpressionManager] = findBulk(
filters.byProps("pushLazy", "popAll"), filters.byProps("pushLazy", "popAll"),
filters.byProps("clearDraft", "saveDraft"), filters.byProps("clearDraft", "saveDraft"),
filters.byProps("DraftType"), filters.byProps("closeExpressionPicker", "openExpressionPicker"),);
filters.byProps("closeExpressionPicker", "openExpressionPicker"),
);
return { return {
ModalStack: modules[0], ModalStack,
DraftManager: modules[1], DraftManager,
DraftType: modules[2]?.DraftType, closeExpressionPicker: ExpressionManager?.closeExpressionPicker,
closeExpressionPicker: modules[3]?.closeExpressionPicker,
}; };
}); });
@ -104,7 +102,7 @@ export default definePlugin({
shouldAttemptRecover = false; shouldAttemptRecover = false;
// This is enough to avoid a crash loop // This is enough to avoid a crash loop
setTimeout(() => shouldAttemptRecover = true, 500); setTimeout(() => shouldAttemptRecover = true, 1000);
} catch { } } catch { }
try { try {
@ -137,8 +135,11 @@ export default definePlugin({
try { try {
const channelId = SelectedChannelStore.getChannelId(); const channelId = SelectedChannelStore.getChannelId();
DraftManager.clearDraft(channelId, DraftType.ChannelMessage); for (const key in DraftType) {
DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage); if (!Number.isNaN(Number(key))) continue;
DraftManager.clearDraft(channelId, DraftType[key]);
}
} catch (err) { } catch (err) {
CrashHandlerLogger.debug("Failed to clear drafts.", err); CrashHandlerLogger.debug("Failed to clear drafts.", err);
} }

View file

@ -0,0 +1,68 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
export default definePlugin({
name: "CtrlEnterSend",
authors: [Devs.UlyssesZhan],
description: "Use Ctrl+Enter to send messages (customizable)",
settings: definePluginSettings({
submitRule: {
description: "The way to send a message",
type: OptionType.SELECT,
options: [
{
label: "Ctrl+Enter (Enter or Shift+Enter for new line)",
value: "ctrl+enter"
},
{
label: "Shift+Enter (Enter for new line)",
value: "shift+enter"
},
{
label: "Enter (Shift+Enter for new line; Discord default)",
value: "enter"
}
],
default: "ctrl+enter"
},
sendMessageInTheMiddleOfACodeBlock: {
description: "Whether to send a message in the middle of a code block",
type: OptionType.BOOLEAN,
default: true,
}
}),
patches: [
{
find: "KeyboardKeys.ENTER&&(!",
replacement: {
match: /(?<=(\i)\.which===\i\.KeyboardKeys.ENTER&&).{0,100}(\(0,\i\.hasOpenPlainTextCodeBlock\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
replace: "$self.shouldSubmit($1, $2)"
}
}
],
shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean {
let result = false;
switch (this.settings.store.submitRule) {
case "shift+enter":
result = event.shiftKey;
break;
case "ctrl+enter":
result = event.ctrlKey;
break;
case "enter":
result = !event.shiftKey && !event.ctrlKey;
break;
}
if (!this.settings.store.sendMessageInTheMiddleOfACodeBlock) {
result &&= !codeblock;
}
return result;
}
});

View file

@ -17,13 +17,16 @@
*/ */
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards"; import { isTruthy } from "@utils/guards";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color"); const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile"); const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
export default definePlugin({ export default definePlugin({
name: "CustomRPC", name: "CustomRPC",
description: "Allows you to set a custom rich presence.", description: "Allows you to set a custom rich presence.",
authors: [Devs.captain, Devs.AutumnVN], authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
start: setRpc, start: setRpc,
stop: () => setRpc(true), stop: () => setRpc(true),
settings, settings,
settingsAboutComponent: () => { settingsAboutComponent: () => {
const activity = useAwaiter(createActivity); const activity = useAwaiter(createActivity);
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
const { profileThemeStyle } = useProfileThemeStyle({}); const { profileThemeStyle } = useProfileThemeStyle({});
return ( return (
<> <>
{!gameActivityEnabled && (
<ErrorCard
className={classes(Margins.top16, Margins.bottom16)}
style={{ padding: "1em" }}
>
<Forms.FormTitle>Notice</Forms.FormTitle>
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
<Button
color={Button.Colors.TRANSPARENT}
className={Margins.top8}
onClick={() => StatusSettingsStores.ShowCurrentGame.updateSetting(true)}
>
Enable
</Button>
</ErrorCard>
)}
<Forms.FormText> <Forms.FormText>
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID. get the application ID.
@ -407,7 +429,9 @@ export default definePlugin({
<Forms.FormText> <Forms.FormText>
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address". If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
</Forms.FormText> </Forms.FormText>
<Forms.FormDivider />
<Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle }}> <div style={{ width: "284px", ...profileThemeStyle }}>
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()} {activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())} guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}

View file

@ -0,0 +1,5 @@
# CustomIdle
Lets you change the time until your status gets automatically set to idle. You can also prevent idling altogether.
![Plugin Configuration](https://github.com/Vendicated/Vencord/assets/45801973/4e5259b2-18e0-42e5-b69f-efc672ce1e0b)

View file

@ -0,0 +1,94 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Notices } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
const settings = definePluginSettings({
idleTimeout: {
description: "Minutes before Discord goes idle (0 to disable auto-idle)",
type: OptionType.SLIDER,
markers: makeRange(0, 60, 5),
default: 10,
stickToMarkers: false,
restartNeeded: true // Because of the setInterval patch
},
remainInIdle: {
description: "When you come back to Discord, remain idle until you confirm you want to go online",
type: OptionType.BOOLEAN,
default: true
}
});
export default definePlugin({
name: "CustomIdle",
description: "Allows you to set the time before Discord goes idle (or disable auto-idle)",
authors: [Devs.newwares],
settings,
patches: [
{
find: "IDLE_DURATION:function(){return",
replacement: {
match: /(IDLE_DURATION:function\(\){return )\i/,
replace: "$1$self.getIdleTimeout()"
}
},
{
find: 'type:"IDLE",idle:',
replacement: [
{
match: /Math\.min\((\i\.AfkTimeout\.getSetting\(\)\*\i\.default\.Millis\.SECOND),\i\.IDLE_DURATION\)/,
replace: "$1" // Decouple idle from afk (phone notifications will remain at user setting or 10 min maximum)
},
{
match: /\i\.default\.dispatch\({type:"IDLE",idle:!1}\)/,
replace: "$self.handleOnline()"
},
{
match: /(setInterval\(\i,\.25\*)\i\.IDLE_DURATION/,
replace: "$1$self.getIntervalDelay()" // For web installs
}
]
}
],
getIntervalDelay() {
return Math.min(6e5, this.getIdleTimeout());
},
handleOnline() {
if (!settings.store.remainInIdle) {
FluxDispatcher.dispatch({
type: "IDLE",
idle: false
});
return;
}
const backOnlineMessage = "Welcome back! Click the button to go online. Click the X to stay idle until reload.";
if (
Notices.currentNotice?.[1] === backOnlineMessage ||
Notices.noticesQueue.some(([, noticeMessage]) => noticeMessage === backOnlineMessage)
) return;
Notices.showNotice(backOnlineMessage, "Exit idle", () => {
Notices.popNotice();
FluxDispatcher.dispatch({
type: "IDLE",
idle: false
});
});
},
getIdleTimeout() { // milliseconds, default is 6e5
const { idleTimeout } = settings.store;
return idleTimeout === 0 ? Infinity : idleTimeout * 60000;
}
});

View file

@ -6,10 +6,11 @@
import "./styles.css"; import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Tooltip } from "@webpack/common"; import { Tooltip } from "@webpack/common";
import type { Component } from "react"; import type { Component } from "react";
@ -34,11 +35,19 @@ interface Props {
}; };
} }
const enum ReplaceElements {
ReplaceAllElements,
ReplaceTitlesOnly,
ReplaceThumbnailsOnly
}
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/; const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
async function embedDidMount(this: Component<Props>) { async function embedDidMount(this: Component<Props>) {
try { try {
const { embed } = this.props; const { embed } = this.props;
const { replaceElements } = settings.store;
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return; if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
const videoId = embedUrlRe.exec(embed.video.url)?.[1]; const videoId = embedUrlRe.exec(embed.video.url)?.[1];
@ -58,12 +67,12 @@ async function embedDidMount(this: Component<Props>) {
enabled: true enabled: true
}; };
if (hasTitle) { if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle; embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1"); embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
} }
if (hasThumb) { if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL; embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`; embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
} }
@ -128,10 +137,30 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
); );
} }
const settings = definePluginSettings({
hideButton: {
description: "Hides the Dearrow button from YouTube embeds",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
replaceElements: {
description: "Choose which elements of the embed will be replaced",
type: OptionType.SELECT,
restartNeeded: true,
options: [
{ label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
],
}
});
export default definePlugin({ export default definePlugin({
name: "Dearrow", name: "Dearrow",
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow", description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
authors: [Devs.Ven], authors: [Devs.Ven],
settings,
embedDidMount, embedDidMount,
renderButton(component: Component<Props>) { renderButton(component: Component<Props>) {
@ -154,7 +183,8 @@ export default definePlugin({
// add dearrow button // add dearrow button
{ {
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/, match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
replace: "children:[$self.renderButton(this)," replace: "children:[$self.renderButton(this),",
predicate: () => !settings.store.hideButton
} }
] ]
}], }],

View file

@ -9,7 +9,6 @@ import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal"; import { openModal } from "@utils/modal";
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common"; import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
import type { StateStorage } from "zustand/middleware";
import { AUTHORIZE_URL, CLIENT_ID } from "../constants"; import { AUTHORIZE_URL, CLIENT_ID } from "../constants";
@ -23,7 +22,7 @@ interface AuthorizationState {
isAuthorized: () => boolean; isAuthorized: () => boolean;
} }
const indexedDBStorage: StateStorage = { const indexedDBStorage = {
async getItem(name: string): Promise<string | null> { async getItem(name: string): Promise<string | null> {
return DataStore.get(name).then(v => v ?? null); return DataStore.get(name).then(v => v ?? null);
}, },
@ -36,9 +35,9 @@ const indexedDBStorage: StateStorage = {
}; };
// TODO: Move switching accounts subscription inside the store? // TODO: Move switching accounts subscription inside the store?
export const useAuthorizationStore = proxyLazy(() => zustandCreate<AuthorizationState>( export const useAuthorizationStore = proxyLazy(() => zustandCreate(
zustandPersist( zustandPersist(
(set, get) => ({ (set: any, get: any) => ({
token: null, token: null,
tokens: {}, tokens: {},
init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); }, init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); },
@ -91,7 +90,7 @@ export const useAuthorizationStore = proxyLazy(() => zustandCreate<Authorization
)); ));
}, },
isAuthorized: () => !!get().token, isAuthorized: () => !!get().token,
}), } as AuthorizationState),
{ {
name: "decor-auth", name: "decor-auth",
getStorage: () => indexedDBStorage, getStorage: () => indexedDBStorage,

View file

@ -21,7 +21,7 @@ interface UserDecorationsState {
clear: () => void; clear: () => void;
} }
export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<UserDecorationsState>((set, get) => ({ export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
decorations: [], decorations: [],
selectedDecoration: null, selectedDecoration: null,
async fetch() { async fetch() {
@ -53,4 +53,4 @@ export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate<User
useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null); useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null);
}, },
clear: () => set({ decorations: [], selectedDecoration: null }) clear: () => set({ decorations: [], selectedDecoration: null })
}))); } as UserDecorationsState)));

View file

@ -30,7 +30,7 @@ interface UsersDecorationsState {
set: (userId: string, decoration: string | null) => void; set: (userId: string, decoration: string | null) => void;
} }
export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecorationsState>((set, get) => ({ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
usersDecorations: new Map<string, UserDecorationData>(), usersDecorations: new Map<string, UserDecorationData>(),
fetchQueue: new Set(), fetchQueue: new Set(),
bulkFetch: debounce(async () => { bulkFetch: debounce(async () => {
@ -40,7 +40,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecor
set({ fetchQueue: new Set() }); set({ fetchQueue: new Set() });
const fetchIds = Array.from(fetchQueue); const fetchIds = [...fetchQueue];
const fetchedUsersDecorations = await getUsersDecorations(fetchIds); const fetchedUsersDecorations = await getUsersDecorations(fetchIds);
const newUsersDecorations = new Map(usersDecorations); const newUsersDecorations = new Map(usersDecorations);
@ -92,7 +92,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate<UsersDecor
newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() }); newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() });
set({ usersDecorations: newUsersDecorations }); set({ usersDecorations: newUsersDecorations });
} }
}))); } as UsersDecorationsState)));
export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined { export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined {
const [decorAvatarDecoration, setDecorAvatarDecoration] = useState<string | null>(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null); const [decorAvatarDecoration, setDecorAvatarDecoration] = useState<string | null>(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null);

View file

@ -15,7 +15,7 @@ import { openChangeDecorationModal } from "../modals/ChangeDecorationModal";
const CustomizationSection = findByCodeLazy(".customizationSectionBackground"); const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
interface DecorSectionProps { export interface DecorSectionProps {
hideTitle?: boolean; hideTitle?: boolean;
hideDivider?: boolean; hideDivider?: boolean;
noMargin?: boolean; noMargin?: boolean;

View file

@ -24,7 +24,7 @@ import { Margins } from "@utils/margins";
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal"; import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common"; import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Promisable } from "type-fest"; import { Promisable } from "type-fest";
const StickersStore = findStoreLazy("StickersStore"); const StickersStore = findStoreLazy("StickersStore");
@ -64,7 +64,7 @@ async function fetchSticker(id: string) {
if (cached) return cached; if (cached) return cached;
const { body } = await RestAPI.get({ const { body } = await RestAPI.get({
url: `/stickers/${id}` url: Constants.Endpoints.STICKER(id)
}); });
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
@ -83,7 +83,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) {
data.append("file", await fetchBlob(getUrl(sticker))); data.append("file", await fetchBlob(getUrl(sticker)));
const { body } = await RestAPI.post({ const { body } = await RestAPI.post({
url: `/guilds/${guildId}/stickers`, url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId),
body: data, body: data,
}); });
@ -322,8 +322,9 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
switch (favoriteableType) { switch (favoriteableType) {
case "emoji": case "emoji":
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`)); const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
if (!match) return; const reaction = props.message.reactions.find(reaction => reaction.emoji.id === favoriteableId);
const name = match[1] ?? "FakeNitroEmoji"; if (!match && !reaction) return;
const name = (match && match[1]) ?? reaction?.emoji.name ?? "FakeNitroEmoji";
return buildMenuItem("Emoji", () => ({ return buildMenuItem("Emoji", () => ({
id: favoriteableId, id: favoriteableId,

View file

@ -24,13 +24,12 @@ import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { CustomEmoji } from "@webpack/types"; import type { Emoji } from "@webpack/types";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react"; import type { ReactElement, ReactNode } from "react";
const DRAFT_TYPE = 0;
const StickerStore = findStoreLazy("StickersStore") as { const StickerStore = findStoreLazy("StickersStore") as {
getPremiumPacks(): StickerPack[]; getPremiumPacks(): StickerPack[];
getAllGuildStickers(): Map<string, Sticker[]>; getAllGuildStickers(): Map<string, Sticker[]>;
@ -39,6 +38,7 @@ const StickerStore = findStoreLazy("StickersStore") as {
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore"); const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS"); const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
const RoleSubscriptionEmojiUtils = findByPropsLazy("isUnusableRoleSubscriptionEmoji");
function searchProtoClassField(localName: string, protoClass: any) { function searchProtoClassField(localName: string, protoClass: any) {
const field = protoClass?.fields?.find((field: any) => field.localName === localName); const field = protoClass?.fields?.find((field: any) => field.localName === localName);
@ -54,16 +54,22 @@ const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoCla
const enum EmojiIntentions { const enum EmojiIntentions {
REACTION = 0, REACTION,
STATUS = 1, STATUS,
COMMUNITY_CONTENT = 2, COMMUNITY_CONTENT,
CHAT = 3, CHAT,
GUILD_STICKER_RELATED_EMOJI = 4, GUILD_STICKER_RELATED_EMOJI,
GUILD_ROLE_BENEFIT_EMOJI = 5, GUILD_ROLE_BENEFIT_EMOJI,
COMMUNITY_CONTENT_ONLY = 6, COMMUNITY_CONTENT_ONLY,
SOUNDBOARD = 7 SOUNDBOARD,
VOICE_CHANNEL_TOPIC,
GIFT,
AUTO_SUGGESTION,
POLLS
} }
const IS_BYPASSEABLE_INTENTION = `[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;
const enum StickerType { const enum StickerType {
PNG = 1, PNG = 1,
APNG = 2, APNG = 2,
@ -111,7 +117,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
const settings = definePluginSettings({ const settings = definePluginSettings({
enableEmojiBypass: { enableEmojiBypass: {
description: "Allow sending fake emojis", description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true, default: true,
restartNeeded: true restartNeeded: true
@ -129,7 +135,7 @@ const settings = definePluginSettings({
restartNeeded: true restartNeeded: true
}, },
enableStickerBypass: { enableStickerBypass: {
description: "Allow sending fake stickers", description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true, default: true,
restartNeeded: true restartNeeded: true
@ -190,7 +196,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
export default definePlugin({ export default definePlugin({
name: "FakeNitro", name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN], authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.", description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
dependencies: ["MessageEventsAPI"], dependencies: ["MessageEventsAPI"],
settings, settings,
@ -198,37 +204,43 @@ export default definePlugin({
patches: [ patches: [
{ {
find: ".PREMIUM_LOCKED;", find: ".PREMIUM_LOCKED;",
group: true,
predicate: () => settings.store.enableEmojiBypass, predicate: () => settings.store.enableEmojiBypass,
replacement: [ replacement: [
{ {
// Create a variable for the intention of listing the emoji // Create a variable for the intention of using the emoji
match: /(?<=,intention:(\i).+?;)/, match: /(?<=\.USE_EXTERNAL_EMOJIS.+?;)(?<=intention:(\i).+?)/,
replace: (_, intention) => `let fakeNitroIntention=${intention};` replace: (_, intention) => `const fakeNitroIntention=${intention};`
}, },
{ {
// Send the intention of listing the emoji to the nitro permission check functions // Disallow the emoji for external if the intention doesn't allow it
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g, match: /&&!\i&&!\i(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0' replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
}, },
{ {
// Disallow the emoji if the intention doesn't allow it // Disallow the emoji for unavailable if the intention doesn't allow it
match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, match: /!\i\.available(?=\)return \i\.\i\.GUILD_SUBSCRIPTION_UNAVAILABLE;)/,
replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))` replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
}, },
{ {
// Make the emoji always available if the intention allows it // Disallow the emoji for premium locked if the intention doesn't allow it
match: /if\(!\i\.available/, match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/,
replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))` replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
},
{
// Allow animated emojis to be used if the intention allows it
match: /(?<=\|\|)\i\.\i\.canUseAnimatedEmojis\(\i\)/,
replace: m => `(${m}||${IS_BYPASSEABLE_INTENTION})`
} }
] ]
}, },
// Allow emojis and animated emojis to be sent everywhere // Allows the usage of subscription-locked emojis
{ {
find: "canUseAnimatedEmojis:function", find: "isUnusableRoleSubscriptionEmoji:function",
predicate: () => settings.store.enableEmojiBypass,
replacement: { replacement: {
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))(?=})/g, match: /isUnusableRoleSubscriptionEmoji:function/,
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` // Replace the original export with a func that always returns false and alias the original
replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
} }
}, },
// Allow stickers to be sent everywhere // Allow stickers to be sent everywhere
@ -242,10 +254,10 @@ export default definePlugin({
}, },
// Make stickers always available // Make stickers always available
{ {
find: "\"SENDABLE\"", find: '"SENDABLE"',
predicate: () => settings.store.enableStickerBypass, predicate: () => settings.store.enableStickerBypass,
replacement: { replacement: {
match: /(\w+)\.available\?/, match: /\i\.available\?/,
replace: "true?" replace: "true?"
} }
}, },
@ -332,8 +344,8 @@ export default definePlugin({
{ {
// Patch the stickers array to add fake nitro stickers // Patch the stickers array to add fake nitro stickers
predicate: () => settings.store.transformStickers, predicate: () => settings.store.transformStickers,
match: /(?<=renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;)/, match: /renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;/,
replace: (_, message, stickers) => `${stickers}=$self.patchFakeNitroStickers(${stickers},${message});` replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
}, },
{ {
// Filter attachments to remove fake nitro stickers or emojis // Filter attachments to remove fake nitro stickers or emojis
@ -797,13 +809,16 @@ export default definePlugin({
gif.finish(); gif.finish();
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" }); const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE); UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);
}, },
canUseEmote(e: CustomEmoji, channelId: string) { canUseEmote(e: Emoji, channelId: string) {
if (e.require_colons === false) return true; if (e.type === "UNICODE") return true;
if (e.available === false) return false; if (e.available === false) return false;
const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
if (isUnusableRoleSubEmoji(e, this.guildId)) return false;
if (this.canUseEmotes) if (this.canUseEmotes)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId); return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
else else

View file

@ -0,0 +1,3 @@
.vc-fpt-preview * {
pointer-events: none;
}

View file

@ -17,13 +17,17 @@
*/ */
// This plugin is a port from Alyxia's Vendetta plugin // This plugin is a port from Alyxia's Vendetta plugin
import "./index.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc"; import { classes, copyWithToast } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, Forms } from "@webpack/common"; import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import virtualMerge from "virtual-merge"; import virtualMerge from "virtual-merge";
@ -81,6 +85,34 @@ const settings = definePluginSettings({
} }
}); });
interface ColorPickerProps {
color: number | null;
label: React.ReactElement;
showEyeDropper?: boolean;
suggestedColors?: string[];
onChange(value: number | null): void;
}
// I can't be bothered to figure out the semantics of this component. The
// functions surely get some event argument sent to them and they likely aren't
// all required. If anyone who wants to use this component stumbles across this
// code, you'll have to do the research yourself.
interface ProfileModalProps {
user: User;
pendingThemeColors: [number, number];
onAvatarChange: () => void;
onBannerChange: () => void;
canUsePremiumCustomization: boolean;
hideExampleButton: boolean;
hideFakeActivity: boolean;
isTryItOutFlow: boolean;
}
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>('"ProfileCustomizationPreview"');
const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i\("(.+?)"\).then\(\i\.bind\(\i,"(.+?)"\)\)/);
export default definePlugin({ export default definePlugin({
name: "FakeProfileThemes", name: "FakeProfileThemes",
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding", description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
@ -101,21 +133,98 @@ export default definePlugin({
} }
} }
], ],
settingsAboutComponent: () => ( settingsAboutComponent: () => {
const existingColors = decode(
UserProfileStore.getUserProfile(UserStore.getCurrentUser().id).bio
) ?? [0, 0];
const [color1, setColor1] = useState(existingColors[0]);
const [color2, setColor2] = useState(existingColors[1]);
const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle> <Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>
After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins. <br /> After enabling this plugin, you will see custom colors in
the profiles of other people using compatible plugins.{" "}
<br />
To set your own colors: To set your own colors:
<ul> <ul>
<li> go to your profile settings</li> <li>
<li> choose your own colors in the Nitro preview</li> use the color pickers below to choose your colors
</li>
<li> click the "Copy 3y3" button</li> <li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li> <li> paste the invisible text anywhere in your bio</li>
</ul><br /> </ul><br />
<b>Please note:</b> if you are using a theme which hides nitro ads, you should disable it temporarily to set colors. <Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
{!loadingColorPickerChunk && (
<Flex
direction={Flex.Direction.HORIZONTAL}
style={{ gap: "1rem" }}
>
<ColorPicker
color={color1}
label={
<Text
variant={"text-xs/normal"}
style={{ marginTop: "4px" }}
>
Primary
</Text>
}
onChange={(color: number) => {
setColor1(color);
}}
/>
<ColorPicker
color={color2}
label={
<Text
variant={"text-xs/normal"}
style={{ marginTop: "4px" }}
>
Accent
</Text>
}
onChange={(color: number) => {
setColor2(color);
}}
/>
<Button
onClick={() => {
const colorString = encode(color1, color2);
copyWithToast(colorString);
}}
color={Button.Colors.PRIMARY}
size={Button.Sizes.XLARGE}
>
Copy 3y3
</Button>
</Flex>
)}
<Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>
<div className="vc-fpt-preview">
<ProfileModal
user={UserStore.getCurrentUser()}
pendingThemeColors={[color1, color2]}
onAvatarChange={() => { }}
onBannerChange={() => { }}
canUsePremiumCustomization={true}
hideExampleButton={true}
hideFakeActivity={true}
isTryItOutFlow={true}
/>
</div>
</Forms.FormText> </Forms.FormText>
</Forms.FormSection>), </Forms.FormSection>);
},
settings, settings,
colorDecodeHook(user: UserProfile) { colorDecodeHook(user: UserProfile) {
if (user) { if (user) {

View file

@ -20,7 +20,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption,
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { RestAPI, UserStore } from "@webpack/common"; import { Constants, RestAPI, UserStore } from "@webpack/common";
const FriendInvites = findByPropsLazy("createFriendInvite"); const FriendInvites = findByPropsLazy("createFriendInvite");
const { uuid4 } = findByPropsLazy("uuid4"); const { uuid4 } = findByPropsLazy("uuid4");
@ -58,7 +58,7 @@ export default definePlugin({
if (uses === 1) { if (uses === 1) {
const random = uuid4(); const random = uuid4();
const { body: { invite_suggestions } } = await RestAPI.post({ const { body: { invite_suggestions } } = await RestAPI.post({
url: "/friend-finder/find-friends", url: Constants.Endpoints.FRIEND_FINDER,
body: { body: {
modified_contacts: { modified_contacts: {
[random]: [1, "", ""] [random]: [1, "", ""]

View file

@ -7,6 +7,8 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord"; import { getCurrentChannel } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Heading, React, RelationshipStore, Text } from "@webpack/common"; import { Heading, React, RelationshipStore, Text } from "@webpack/common";
@ -22,6 +24,7 @@ export default definePlugin({
description: "Shows when you became friends with someone in the user popout", description: "Shows when you became friends with someone in the user popout",
authors: [Devs.Elvyra], authors: [Devs.Elvyra],
patches: [ patches: [
// User popup
{ {
find: ".AnalyticsSections.USER_PROFILE}", find: ".AnalyticsSections.USER_PROFILE}",
replacement: { replacement: {
@ -29,16 +32,34 @@ export default definePlugin({
replace: "$&,$self.friendsSince({ userId: $1 })" replace: "$&,$self.friendsSince({ userId: $1 })"
} }
}, },
// User DMs "User Profile" popup in the right
{ {
find: ".UserPopoutUpsellSource.PROFILE_PANEL,", find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
replacement: { replacement: {
match: /\i.default,\{userId:(\i)}\)/, match: /\i.default,\{userId:(\i)}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })" replace: "$&,$self.friendsSince({ userId: $1 })"
} }
},
// User Profile Modal
{
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.friendsSince({ userId: ${userId}, textClassName: ${textClassName} })`
}
} }
], ],
friendsSince: ErrorBoundary.wrap(({ userId }: { userId: string; }) => { getFriendSince(userId: string) {
try {
return RelationshipStore.getSince(userId);
} catch (err) {
new Logger("FriendsSince").error(err);
return null;
}
},
friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
const friendsSince = RelationshipStore.getSince(userId); const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null; if (!friendsSince) return null;
@ -61,7 +82,7 @@ export default definePlugin({
<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" /> <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> </svg>
)} )}
<Text variant="text-sm/normal" className={clydeMoreInfo.body}> <Text variant="text-sm/normal" className={classes(clydeMoreInfo.body, textClassName)}>
{getCreatedAtDate(friendsSince, locale.getLocale())} {getCreatedAtDate(friendsSince, locale.getLocale())}
</Text> </Text>
</div> </div>
@ -69,4 +90,3 @@ export default definePlugin({
); );
}, { noop: true }) }, { noop: true })
}); });

View file

@ -228,15 +228,15 @@ export default definePlugin({
{ {
find: ".activityTitleText,variant", find: ".activityTitleText,variant",
replacement: { replacement: {
match: /(?<=\i\.activityTitleText.+?children:(\i)\.name.*?}\),)/, match: /\.activityTitleText.+?children:(\i)\.name.*?}\),/,
replace: (_, props) => `$self.renderToggleActivityButton(${props}),` replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
}, },
}, },
{ {
find: ".activityCardDetails,children", find: ".activityCardDetails,children",
replacement: { replacement: {
match: /(?<=\i\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),)/, match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
replace: (_, props) => `$self.renderToggleActivityButton(${props}),` replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
} }
} }
], ],

View file

@ -17,6 +17,7 @@
*/ */
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { FluxDispatcher, React, useRef, useState } from "@webpack/common"; import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
import { ELEMENT_ID } from "../constants"; import { ELEMENT_ID } from "../constants";
@ -36,7 +37,7 @@ export interface MagnifierProps {
const cl = classNameFactory("vc-imgzoom-"); const cl = classNameFactory("vc-imgzoom-");
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => { export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: initialSize, zoom: initalZoom }) => {
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 }); const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
@ -199,4 +200,4 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
)} )}
</div> </div>
); );
}; }, { noop: true });

View file

@ -20,6 +20,7 @@ import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeFind } from "@utils/patches";
import { Patch, Plugin, StartAt } from "@utils/types"; import { Patch, Plugin, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
@ -83,8 +84,12 @@ for (const p of pluginsValues) {
if (p.patches && isPluginEnabled(p.name)) { if (p.patches && isPluginEnabled(p.name)) {
for (const patch of p.patches) { for (const patch of p.patches) {
patch.plugin = p.name; patch.plugin = p.name;
if (!Array.isArray(patch.replacement))
canonicalizeFind(patch);
if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement]; patch.replacement = [patch.replacement];
}
patches.push(patch); patches.push(patch);
} }
} }
@ -165,13 +170,14 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
} }
try { try {
p.start(); p.start();
p.started = true;
} catch (e) { } catch (e) {
logger.error(`Failed to start ${name}\n`, e); logger.error(`Failed to start ${name}\n`, e);
return false; return false;
} }
} }
p.started = true;
if (commands?.length) { if (commands?.length) {
logger.debug("Registering commands of plugin", name); logger.debug("Registering commands of plugin", name);
for (const cmd of commands) { for (const cmd of commands) {
@ -201,6 +207,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) { export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
const { name, commands, flux, contextMenus } = p; const { name, commands, flux, contextMenus } = p;
if (p.stop) { if (p.stop) {
logger.info("Stopping plugin", name); logger.info("Stopping plugin", name);
if (!p.started) { if (!p.started) {
@ -209,13 +216,14 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
} }
try { try {
p.stop(); p.stop();
p.started = false;
} catch (e) { } catch (e) {
logger.error(`Failed to stop ${name}\n`, e); logger.error(`Failed to stop ${name}\n`, e);
return false; return false;
} }
} }
p.started = false;
if (commands?.length) { if (commands?.length) {
logger.debug("Unregistering commands of plugin", name); logger.debug("Unregistering commands of plugin", name);
for (const cmd of commands) { for (const cmd of commands) {

View file

@ -23,7 +23,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getStegCloak } from "@utils/dependencies"; import { getStegCloak } from "@utils/dependencies";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
import { buildDecModal } from "./components/DecryptionModal"; import { buildDecModal } from "./components/DecryptionModal";
@ -153,7 +153,7 @@ export default definePlugin({
// Gets the Embed of a Link // Gets the Embed of a Link
async getEmbed(url: URL): Promise<Object | {}> { async getEmbed(url: URL): Promise<Object | {}> {
const { body } = await RestAPI.post({ const { body } = await RestAPI.post({
url: "/unfurler/embed-urls", url: Constants.Endpoints.UNFURL_EMBED_URLS,
body: { body: {
urls: [url] urls: [url]
} }

View file

@ -114,6 +114,11 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: false, default: false,
}, },
shareSong: {
description: "show link to song on last.fm",
type: OptionType.BOOLEAN,
default: true,
},
hideWithSpotify: { hideWithSpotify: {
description: "hide last.fm presence if spotify is running", description: "hide last.fm presence if spotify is running",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -295,12 +300,7 @@ export default definePlugin({
large_text: trackData.album || undefined, large_text: trackData.album || undefined,
}; };
const buttons: ActivityButton[] = [ const buttons: ActivityButton[] = [];
{
label: "View Song",
url: trackData.url,
},
];
if (settings.store.shareUsername) if (settings.store.shareUsername)
buttons.push({ buttons.push({
@ -308,6 +308,12 @@ export default definePlugin({
url: `https://www.last.fm/user/${settings.store.username}`, url: `https://www.last.fm/user/${settings.store.username}`,
}); });
if (settings.store.shareSong)
buttons.push({
label: "View Song",
url: trackData.url,
});
const statusName = (() => { const statusName = (() => {
switch (settings.store.nameFormat) { switch (settings.store.nameFormat) {
case NameFormat.ArtistFirst: case NameFormat.ArtistFirst:
@ -333,7 +339,7 @@ export default definePlugin({
state: trackData.artist, state: trackData.artist,
assets, assets,
buttons: buttons.map(v => v.label), buttons: buttons.length ? buttons.map(v => v.label) : undefined,
metadata: { metadata: {
button_urls: buttons.map(v => v.url), button_urls: buttons.map(v => v.url),
}, },

View file

@ -70,8 +70,8 @@ export default definePlugin({
{ {
find: ".invitesDisabledTooltip", find: ".invitesDisabledTooltip",
replacement: { replacement: {
match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/, match: /\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100}(?=])/,
replace: ",$self.renderTooltip(arguments[0].guild)]" replace: "$&,$self.renderTooltip(arguments[0].guild)"
}, },
predicate: () => settings.store.toolTip predicate: () => settings.store.toolTip
} }

View file

@ -13,7 +13,7 @@ import { findExportedComponentLazy } from "@webpack";
import { SnowflakeUtils, Tooltip } from "@webpack/common"; import { SnowflakeUtils, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
type FillValue = ("status-danger" | "status-warning" | "text-muted"); type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
type Fill = [FillValue, FillValue, FillValue]; type Fill = [FillValue, FillValue, FillValue];
type DiffKey = keyof Diff; type DiffKey = keyof Diff;
@ -22,21 +22,35 @@ interface Diff {
hours: number, hours: number,
minutes: number, minutes: number,
seconds: number; seconds: number;
milliseconds: number;
} }
const DISCORD_KT_DELAY = 1471228928;
const HiddenVisually = findExportedComponentLazy("HiddenVisually"); const HiddenVisually = findExportedComponentLazy("HiddenVisually");
export default definePlugin({ export default definePlugin({
name: "MessageLatency", name: "MessageLatency",
description: "Displays an indicator for messages that took ≥n seconds to send", description: "Displays an indicator for messages that took ≥n seconds to send",
authors: [Devs.arHSM], authors: [Devs.arHSM],
settings: definePluginSettings({ settings: definePluginSettings({
latency: { latency: {
type: OptionType.NUMBER, type: OptionType.NUMBER,
description: "Threshold in seconds for latency indicator", description: "Threshold in seconds for latency indicator",
default: 2 default: 2
},
detectDiscordKotlin: {
type: OptionType.BOOLEAN,
description: "Detect old Discord Android clients",
default: true
},
showMillis: {
type: OptionType.BOOLEAN,
description: "Show milliseconds",
default: false
} }
}), }),
patches: [ patches: [
{ {
find: "showCommunicationDisabledStyles", find: "showCommunicationDisabledStyles",
@ -46,53 +60,95 @@ export default definePlugin({
} }
} }
], ],
stringDelta(delta: number) {
stringDelta(delta: number, showMillis: boolean) {
const diff: Diff = { const diff: Diff = {
days: Math.round(delta / (60 * 60 * 24)), days: Math.round(delta / (60 * 60 * 24 * 1000)),
hours: Math.round((delta / (60 * 60)) % 24), hours: Math.round((delta / (60 * 60 * 1000)) % 24),
minutes: Math.round((delta / (60)) % 60), minutes: Math.round((delta / (60 * 1000)) % 60),
seconds: Math.round(delta % 60), seconds: Math.round(delta / 1000 % 60),
milliseconds: Math.round(delta % 1000)
}; };
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${k}` : null; const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
const keys = Object.keys(diff) as DiffKey[]; const keys = Object.keys(diff) as DiffKey[];
return keys.map(str).filter(isNonNullish).join(" ") || "0 seconds"; const ts = keys.reduce((prev, k) => {
const s = str(k);
return prev + (
isNonNullish(s)
? (prev !== ""
? (showMillis ? k === "milliseconds" : k === "seconds")
? " and "
: " "
: "") + s
: ""
);
}, "");
return ts || "0 seconds";
}, },
latencyTooltipData(message: Message) { latencyTooltipData(message: Message) {
const { latency, detectDiscordKotlin, showMillis } = this.settings.store;
const { id, nonce } = message; const { id, nonce } = message;
// Message wasn't received through gateway // Message wasn't received through gateway
if (!isNonNullish(nonce)) return null; if (!isNonNullish(nonce)) return null;
const delta = Math.round((SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce)) / 1000); let isDiscordKotlin = false;
let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds
if (!showMillis) {
delta = Math.round(delta / 1000) * 1000;
}
// Old Discord Android clients have a delay of around 17 days
// This is a workaround for that
if (-delta >= DISCORD_KT_DELAY - 86400000) { // One day of padding for good measure
isDiscordKotlin = detectDiscordKotlin;
delta += DISCORD_KT_DELAY;
}
// Thanks dziurwa (I hate you) // Thanks dziurwa (I hate you)
// This is when the user's clock is ahead // This is when the user's clock is ahead
// Can't do anything if the clock is behind // Can't do anything if the clock is behind
const abs = Math.abs(delta); const abs = Math.abs(delta);
const ahead = abs !== delta; const ahead = abs !== delta;
const latencyMillis = latency * 1000;
const stringDelta = this.stringDelta(abs); const stringDelta = abs >= latencyMillis ? this.stringDelta(abs, showMillis) : null;
// Also thanks dziurwa // Also thanks dziurwa
// 2 minutes // 2 minutes
const TROLL_LIMIT = 2 * 60; const TROLL_LIMIT = 2 * 60 * 1000;
const { latency } = this.settings.store;
const fill: Fill = delta >= TROLL_LIMIT || ahead ? ["text-muted", "text-muted", "text-muted"] : delta >= (latency * 2) ? ["status-danger", "text-muted", "text-muted"] : ["status-warning", "status-warning", "text-muted"]; const fill: Fill = isDiscordKotlin
? ["status-positive", "status-positive", "text-muted"]
: delta >= TROLL_LIMIT || ahead
? ["text-muted", "text-muted", "text-muted"]
: delta >= (latencyMillis * 2)
? ["status-danger", "text-muted", "text-muted"]
: ["status-warning", "status-warning", "text-muted"];
return abs >= latency ? { delta: stringDelta, ahead: abs !== delta, fill } : null; return (abs >= latencyMillis || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
}, },
Tooltip() { Tooltip() {
return ErrorBoundary.wrap(({ message }: { message: Message; }) => { return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
const d = this.latencyTooltipData(message); const d = this.latencyTooltipData(message);
if (!isNonNullish(d)) return null; if (!isNonNullish(d)) return null;
let text: string;
if (!d.delta) {
text = "User is suspected to be on an old Discord Android client";
} else {
text = (d.ahead ? `This user's clock is ${d.delta} ahead.` : `This message was sent with a delay of ${d.delta}.`) + (d.isDiscordKotlin ? " User is suspected to be on an old Discord Android client." : "");
}
return <Tooltip return <Tooltip
text={d.ahead ? `This user's clock is ${d.delta} ahead` : `This message was sent with a delay of ${d.delta}.`} text={text}
position="top" position="top"
> >
{ {
@ -105,8 +161,9 @@ export default definePlugin({
</Tooltip>; </Tooltip>;
}); });
}, },
Icon({ delta, fill, props }: { Icon({ delta, fill, props }: {
delta: string; delta: string | null;
fill: Fill, fill: Fill,
props: { props: {
onClick(): void; onClick(): void;
@ -126,7 +183,7 @@ export default definePlugin({
role="img" role="img"
fill="none" fill="none"
style={{ marginRight: "8px", verticalAlign: -1 }} style={{ marginRight: "8px", verticalAlign: -1 }}
aria-label={delta} aria-label={delta ?? "Old Discord Android client"}
aria-hidden="false" aria-hidden="false"
{...props} {...props}
> >

View file

@ -27,6 +27,7 @@ import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { import {
Button, Button,
ChannelStore, ChannelStore,
Constants,
FluxDispatcher, FluxDispatcher,
GuildStore, GuildStore,
IconUtils, IconUtils,
@ -132,7 +133,7 @@ async function fetchMessage(channelID: string, messageID: string) {
messageCache.set(messageID, { fetched: false }); messageCache.set(messageID, { fetched: false });
const res = await RestAPI.get({ const res = await RestAPI.get({
url: `/channels/${channelID}/messages`, url: Constants.Endpoints.MESSAGES(channelID),
query: { query: {
limit: 1, limit: 1,
around: messageID around: messageID
@ -226,10 +227,8 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
const accessories = [] as (JSX.Element | null)[]; const accessories = [] as (JSX.Element | null)[];
let match = null as RegExpMatchArray | null; for (const [_, channelID, messageID] of message.content!.matchAll(messageLinkRegex)) {
while ((match = messageLinkRegex.exec(message.content!)) !== null) { if (embeddedBy.includes(messageID) || embeddedBy.length > 2) {
const [_, channelID, messageID] = match;
if (embeddedBy.includes(messageID)) {
continue; continue;
} }

View file

@ -1,3 +1,3 @@
.messagelogger-deleted { .messagelogger-deleted {
background-color: rgba(240 71 71 / 15%) !important; background-color: hsla(var(--red-430-hsl, 0 85% 61%) / 15%) !important;
} }

View file

@ -1,19 +1,19 @@
/* Message content highlighting */ /* Message content highlighting */
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) { .messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
color: #f04747 !important; color: var(--status-danger, #f04747) !important;
} }
/* Bot "thinking" text highlighting */ /* Bot "thinking" text highlighting */
.messagelogger-deleted [class*="colorStandard"] { .messagelogger-deleted [class*="colorStandard"] {
color: #f04747 !important; color: var(--status-danger, #f04747) !important;
} }
/* Embed highlighting */ /* Embed highlighting */
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) { .messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
color: #f04747 !important; color: var(--status-danger, #f04747) !important;
} }
.messagelogger-deleted a { .messagelogger-deleted a {
color: #be3535 !important; color: var(--red-460, #be3535) !important;
text-decoration: underline; text-decoration: underline;
} }

View file

@ -295,12 +295,9 @@ export default definePlugin({
// }, // },
{ {
// Pass through editHistory & deleted & original attachments to the "edited message" transformer // Pass through editHistory & deleted & original attachments to the "edited message" transformer
match: /interactionData:(\i)\.interactionData/, match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
replace: replace:
"interactionData:$1.interactionData," + "Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, attachments:$1.attachments })"
"deleted:$1.deleted," +
"editHistory:$1.editHistory," +
"attachments:$1.attachments"
}, },
// { // {

View file

@ -8,7 +8,7 @@
.emoji, .emoji,
[data-type="sticker"], [data-type="sticker"],
iframe, iframe,
.messagelogger-deleted-attachment, .messagelogger-deleted-attachment:not([class*="hiddenAttachment_"]),
[class|="inlineMediaEmbed"] [class|="inlineMediaEmbed"]
) { ) {
filter: grayscale(1) !important; filter: grayscale(1) !important;

View file

@ -50,6 +50,7 @@ interface TagSettings {
MODERATOR_STAFF: TagSetting, MODERATOR_STAFF: TagSetting,
MODERATOR: TagSetting, MODERATOR: TagSetting,
VOICE_MODERATOR: TagSetting, VOICE_MODERATOR: TagSetting,
TRIAL_MODERATOR: TagSetting,
[k: string]: TagSetting; [k: string]: TagSetting;
} }
@ -93,6 +94,11 @@ const tags: Tag[] = [
displayName: "VC Mod", displayName: "VC Mod",
description: "Can manage voice chats", description: "Can manage voice chats",
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"] permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
}, {
name: "CHAT_MODERATOR",
displayName: "Chat Mod",
description: "Can timeout people",
permissions: ["MODERATE_MEMBERS"]
} }
]; ];
const defaultSettings = Object.fromEntries( const defaultSettings = Object.fromEntries(
@ -263,34 +269,14 @@ export default definePlugin({
], ],
start() { start() {
if (settings.store.tagSettings) return; settings.store.tagSettings ??= defaultSettings;
// @ts-ignore
if (!settings.store.visibility_WEBHOOK) settings.store.tagSettings = defaultSettings; // newly added field might be missing from old users
else { settings.store.tagSettings.CHAT_MODERATOR ??= {
const newSettings = { ...defaultSettings }; text: "Chat Mod",
Object.entries(Vencord.PlainSettings.plugins.MoreUserTags).forEach(([name, value]) => { showInChat: true,
const [setting, tag] = name.split("_"); showInNotChat: true
if (setting === "visibility") { };
switch (value) {
case "always":
// its the default
break;
case "chat":
newSettings[tag].showInNotChat = false;
break;
case "not-chat":
newSettings[tag].showInChat = false;
break;
case "never":
newSettings[tag].showInChat = false;
newSettings[tag].showInNotChat = false;
break;
}
}
settings.store.tagSettings = newSettings;
delete Vencord.Settings.plugins.MoreUserTags[name];
});
}
}, },
getPermissions(user: User, channel: Channel): string[] { getPermissions(user: User, channel: Channel): string[] {
@ -368,6 +354,15 @@ export default definePlugin({
if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue; if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue;
if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue; if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue;
// If the owner tag is disabled, and the user is the owner of the guild,
// avoid adding other tags because the owner will always match the condition for them
if (
tag.name !== "OWNER" &&
GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id &&
(location === "chat" && !settings.tagSettings.OWNER.showInChat) ||
(location === "not-chat" && !settings.tagSettings.OWNER.showInNotChat)
) continue;
if ( if (
tag.permissions?.some(perm => perms.includes(perm)) || tag.permissions?.some(perm => perms.includes(perm)) ||
(tag.condition?.(message!, user, channel)) (tag.condition?.(message!, user, channel))

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards"; import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
@ -55,12 +56,12 @@ export default definePlugin({
find: ".UserProfileSections.USER_INFO_CONNECTIONS:", find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
replacement: { replacement: {
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/, match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);" replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs({user: $1, onClose: $2});"
} }
} }
], ],
renderMutualGDMs(user: User, onClose: () => void) { renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => ( const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
<Clickable <Clickable
className={ProfileListClasses.listRow} className={ProfileListClasses.listRow}
@ -99,5 +100,5 @@ export default definePlugin({
} }
</ScrollerThin> </ScrollerThin>
); );
} })
}); });

View file

@ -0,0 +1,5 @@
# 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,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: "NoDefaultHangStatus",
description: "Disable the default hang status when joining voice channels",
authors: [Devs.D3SOX],
patches: [
{
find: "HangStatusTypes.CHILLING)",
replacement: {
match: /{enableHangStatus:(\i),/,
replace: "{_enableHangStatus:$1=false,"
}
}
]
});

View file

@ -0,0 +1,50 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
shownEmojis: {
description: "The types of emojis to show in the autocomplete menu.",
type: OptionType.SELECT,
default: "onlyUnicode",
options: [
{ label: "Only unicode emojis", value: "onlyUnicode" },
{ label: "Unicode emojis and server emojis from current server", value: "currentServer" },
{ label: "Unicode emojis and all server emojis (Discord default)", value: "all" }
]
}
});
export default definePlugin({
name: "NoServerEmojis",
authors: [Devs.UlyssesZhan],
description: "Do not show server emojis in the autocomplete menu.",
settings,
patches: [
{
find: "}searchWithoutFetchingLatest(",
replacement: {
match: /searchWithoutFetchingLatest.{20,300}get\((\i).{10,40}?reduce\(\((\i),(\i)\)=>\{/,
replace: "$& if ($self.shouldSkip($1, $3)) return $2;"
}
}
],
shouldSkip(guildId: string, emoji: any) {
if (emoji.type !== "GUILD_EMOJI") {
return false;
}
if (settings.store.shownEmojis === "onlyUnicode") {
return true;
}
if (settings.store.shownEmojis === "currentServer") {
return emoji.guildId !== guildId;
}
return false;
}
});

View file

@ -26,6 +26,7 @@ const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/; const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/; const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/; const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/;
const settings = definePluginSettings({ const settings = definePluginSettings({
spotify: { spotify: {
@ -42,6 +43,11 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Open Epic Games links in the Epic Games Launcher", description: "Open Epic Games links in the Epic Games Launcher",
default: true, default: true,
},
tidal: {
type: OptionType.BOOLEAN,
description: "Open Tidal links in the Tidal app",
default: true,
} }
}); });
@ -49,7 +55,7 @@ const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof impo
export default definePlugin({ export default definePlugin({
name: "OpenInApp", name: "OpenInApp",
description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser", description: "Open Spotify, Tidal, Steam and Epic Games URLs in their respective apps instead of your browser",
authors: [Devs.Ven], authors: [Devs.Ven],
settings, settings,
@ -127,6 +133,19 @@ export default definePlugin({
return true; return true;
} }
tidal: {
if (!settings.store.tidal) break tidal;
const match = TidalMatcher.exec(url);
if (!match) break tidal;
const [, type, id] = match;
VencordNative.native.openExternal(`tidal://${type}/${id}`);
event?.preventDefault();
return true;
}
// in case short url didn't end up being something we can handle // in case short url didn't end up being something we can handle
if (event?.defaultPrevented) { if (event?.defaultPrevented) {
window.open(url, "_blank"); window.open(url, "_blank");

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
@ -41,8 +41,9 @@ const settings = definePluginSettings({
}, },
}); });
migratePluginSettings("PartyMode", "Party mode 🎉");
export default definePlugin({ export default definePlugin({
name: "Party mode 🎉", name: "PartyMode",
description: "Allows you to use party mode cause the party never ends ✨", description: "Allows you to use party mode cause the party never ends ✨",
authors: [Devs.UwUDev], authors: [Devs.UwUDev],
settings, settings,

View file

@ -16,14 +16,30 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { GuildStore, RestAPI } from "@webpack/common"; import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
const Messages = findByPropsLazy("GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION");
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment"); const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
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");
}
function disableInvites(guildId: string) {
const guild = GuildStore.getGuild(guildId);
const features = [...guild.features, "INVITES_DISABLED"];
RestAPI.patch({
url: Constants.Endpoints.GUILD(guildId),
body: { features },
});
}
export default definePlugin({ export default definePlugin({
name: "PauseInvitesForever", name: "PauseInvitesForever",
tags: ["DisableInvitesForever"], tags: ["DisableInvitesForever"],
@ -33,42 +49,29 @@ export default definePlugin({
patches: [ patches: [
{ {
find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION", find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION",
replacement: [{ group: true,
replacement: [
{
match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/, match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/,
replace: "children: $self.renderInvitesLabel(arguments[0].guildId, setChecked)", replace: "children: $self.renderInvitesLabel({guildId:arguments[0].guildId,setChecked})",
}, },
{ {
match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/, match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/,
replace: "$1,setChecked=$2" replace: "$1,setChecked=$2"
}] }
]
} }
], ],
showDisableInvites(guildId: string) { renderInvitesLabel: ErrorBoundary.wrap(({ guildId, setChecked }) => {
// 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");
},
disableInvites(guildId: string) {
const guild = GuildStore.getGuild(guildId);
const features = [...guild.features, "INVITES_DISABLED"];
RestAPI.patch({
url: `/guilds/${guild.id}`,
body: { features },
});
},
renderInvitesLabel(guildId: string, setChecked: Function) {
return ( return (
<div> <div>
{Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION} {i18n.Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}
{this.showDisableInvites(guildId) && <a role="button" onClick={() => { {showDisableInvites(guildId) && <a role="button" onClick={() => {
setChecked(true); setChecked(true);
this.disableInvites(guildId); disableInvites(guildId);
}}> Pause Indefinitely.</a>} }}> Pause Indefinitely.</a>}
</div> </div>
); );
} })
}); });

View file

@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord"; import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; 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 type { Guild } from "discord-types/general";
import { settings } from ".."; import { settings } from "..";
@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
<div <div
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })} className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
onContextMenu={e => { onContextMenu={e => {
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role) if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => ( ContextMenuApi.openContextMenu(e, () => (
<RoleContextMenu <RoleContextMenu
guild={guild} guild={guild}
@ -120,6 +120,14 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
onClose={modalProps.onClose} onClose={modalProps.onClose}
/> />
)); ));
else if (permission.type === PermissionType.User) {
ContextMenuApi.openContextMenu(e, () => (
<UserContextMenu
userId={permission.id!}
onClose={modalProps.onClose}
/>
));
}
}} }}
> >
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && ( {(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
@ -199,9 +207,18 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
onClose={ContextMenuApi.closeContextMenu} onClose={ContextMenuApi.closeContextMenu}
aria-label="Role Options" aria-label="Role Options"
> >
<Menu.MenuItem
id="vc-copy-role-id"
label={i18n.Messages.COPY_ID_ROLE}
action={() => {
Clipboard.copy(roleId);
}}
/>
{(settings.store as any).unsafeViewAsRole && (
<Menu.MenuItem <Menu.MenuItem
id="vc-pw-view-as-role" id="vc-pw-view-as-role"
label="View As Role" label={i18n.Messages.VIEW_AS_ROLE}
action={() => { action={() => {
const role = GuildStore.getRole(guild.id, roleId); const role = GuildStore.getRole(guild.id, roleId);
if (!role) return; if (!role) return;
@ -218,6 +235,26 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
} }
} }
}); });
}
}
/>
)}
</Menu.Menu>
);
}
function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
return (
<Menu.Menu
navId={cl("user-context-menu")}
onClose={ContextMenuApi.closeContextMenu}
aria-label="User Options"
>
<Menu.MenuItem
id="vc-copy-user-id"
label={i18n.Messages.COPY_ID_USER}
action={() => {
Clipboard.copy(userId);
}} }}
/> />
</Menu.Menu> </Menu.Menu>

View file

@ -17,7 +17,7 @@
*/ */
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader"; import { ExpandableHeader } from "@components/ExpandableHeader";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { filters, findBulk, proxyLazyWebpack } from "@webpack"; import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common"; import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common";

View file

@ -21,10 +21,9 @@ import { Devs } from "@utils/constants";
import { makeLazy } from "@utils/lazy"; import { makeLazy } from "@utils/lazy";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { UploadHandler, UserUtils } from "@webpack/common"; import { DraftType, UploadHandler, UploadManager, UserUtils } from "@webpack/common";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
const DRAFT_TYPE = 0;
const DEFAULT_DELAY = 20; const DEFAULT_DELAY = 20;
const DEFAULT_RESOLUTION = 128; const DEFAULT_RESOLUTION = 128;
const FRAMES = 10; const FRAMES = 10;
@ -59,9 +58,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
for (const opt of options) { for (const opt of options) {
switch (opt.name) { switch (opt.name) {
case "image": case "image":
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0]; const upload = UploadStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);
if (upload) { if (upload) {
if (!upload.isImage) throw "Upload is not an image"; if (!upload.isImage) {
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
throw "Upload is not an image";
}
return upload.item.file; return upload.item.file;
} }
break; break;
@ -73,10 +75,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048"); return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
} catch (err) { } catch (err) {
console.error("[petpet] Failed to fetch user\n", err); console.error("[petpet] Failed to fetch user\n", err);
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
throw "Failed to fetch user. Check the console for more info."; throw "Failed to fetch user. Check the console for more info.";
} }
} }
} }
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
return null; return null;
} }
@ -130,6 +134,7 @@ export default definePlugin({
var url = await resolveImage(opts, cmdCtx, noServerPfp); var url = await resolveImage(opts, cmdCtx, noServerPfp);
if (!url) throw "No Image specified!"; if (!url) throw "No Image specified!";
} catch (err) { } catch (err) {
UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
sendBotMessage(cmdCtx.channel.id, { sendBotMessage(cmdCtx.channel.id, {
content: String(err), content: String(err),
}); });
@ -147,6 +152,8 @@ export default definePlugin({
canvas.width = canvas.height = resolution; canvas.width = canvas.height = resolution;
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
for (let i = 0; i < FRAMES; i++) { for (let i = 0; i < FRAMES; i++) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -174,7 +181,7 @@ export default definePlugin({
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" }); const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
// Immediately after the command finishes, Discord clears all input, including pending attachments. // Immediately after the command finishes, Discord clears all input, including pending attachments.
// Thus, setTimeout is needed to make this execute after Discord cleared the input // Thus, setTimeout is needed to make this execute after Discord cleared the input
setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10); setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
}, },
}, },
] ]

View file

@ -83,7 +83,7 @@ export default definePlugin({
// Rendering // Rendering
{ {
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/, match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);" replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2)();"
}, },
{ {
match: /"renderSection",(\i)=>{/, match: /"renderSection",(\i)=>{/,
@ -111,8 +111,8 @@ export default definePlugin({
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)" replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
}, },
{ {
match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/, match: /(scrollToChannel\(\i\){.{1,300})(this\.props\.privateChannelIds)/,
replace: "[...$&,...$self.getAllUncollapsedChannels()]" replace: "$1[...$2,...$self.getAllUncollapsedChannels()]"
}, },
] ]
@ -320,9 +320,10 @@ export default definePlugin({
</svg> </svg>
</h2> </h2>
); );
}), }, { noop: true }),
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) { renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
return ErrorBoundary.wrap(() => {
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels); const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
if (!channel || !category) return null; if (!channel || !category) return null;
@ -336,9 +337,9 @@ export default definePlugin({
{channel.id} {channel.id}
</ChannelComponent> </ChannelComponent>
); );
}, { noop: true });
}, },
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) { getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
const category = categories[sectionIndex - 1]; const category = categories[sectionIndex - 1];
if (!category) return { channel: null, category: null }; if (!category) return { channel: null, category: null };

View file

@ -33,7 +33,7 @@ const PRONOUN_TOOLTIP_PATCH = {
export default definePlugin({ export default definePlugin({
name: "PronounDB", name: "PronounDB",
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven], authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven, Devs.Elvyra],
description: "Adds pronouns to user messages using pronoundb", description: "Adds pronouns to user messages using pronoundb",
patches: [ patches: [
{ {

View file

@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react";
import { UserProfileStore, UserStore } from "@webpack/common"; import { UserProfileStore, UserStore } from "@webpack/common";
import { settings } from "./settings"; import { settings } from "./settings";
import { PronounCode, PronounMapping, PronounsResponse } from "./types"; import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types";
type PronounsWithSource = [string | null, string]; type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""]; const EmptyPronouns: PronounsWithSource = [null, ""];
@ -40,9 +40,9 @@ export const enum PronounSource {
} }
// A map of cached pronouns so the same request isn't sent twice // A map of cached pronouns so the same request isn't sent twice
const cache: Record<string, PronounCode> = {}; const cache: Record<string, CachePronouns> = {};
// A map of ids and callbacks that should be triggered on fetch // A map of ids and callbacks that should be triggered on fetch
const requestQueue: Record<string, ((pronouns: PronounCode) => void)[]> = {}; const requestQueue: Record<string, ((pronouns: string) => void)[]> = {};
// Executes all queued requests and calls their callbacks // Executes all queued requests and calls their callbacks
const bulkFetch = debounce(async () => { const bulkFetch = debounce(async () => {
@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => {
const pronouns = await bulkFetchPronouns(ids); const pronouns = await bulkFetchPronouns(ids);
for (const id of ids) { for (const id of ids) {
// Call all callbacks for the id // Call all callbacks for the id
requestQueue[id]?.forEach(c => c(pronouns[id])); requestQueue[id]?.forEach(c => c(pronouns[id] ? extractPronouns(pronouns[id].sets) : ""));
delete requestQueue[id]; delete requestQueue[id];
} }
}); });
@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns) if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
return [discordPronouns, "Discord"]; return [discordPronouns, "Discord"];
if (result && result !== "unspecified") if (result && result !== PronounMapping.unspecified)
return [formatPronouns(result), "PronounDB"]; return [result, "PronounDB"];
return [discordPronouns, "Discord"]; return [discordPronouns, "Discord"];
} }
@ -98,8 +98,9 @@ const NewLineRe = /\n+/g;
// Gets the cached pronouns, if you're too impatient for a promise! // Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string): string | null { export function getCachedPronouns(id: string): string | null {
const cached = cache[id]; const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined;
if (cached && cached !== "unspecified") return cached;
if (cached && cached !== PronounMapping.unspecified) return cached;
return cached || null; return cached || null;
} }
@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
params.append("ids", ids.join(",")); params.append("ids", ids.join(","));
try { try {
const req = await fetch("https://pronoundb.org/api/v1/lookup-bulk?" + params.toString(), { const req = await fetch("https://pronoundb.org/api/v2/lookup?" + params.toString(), {
method: "GET", method: "GET",
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
} catch (e) { } catch (e) {
// If the request errors, treat it as if no pronouns were found for all ids, and log it // If the request errors, treat it as if no pronouns were found for all ids, and log it
console.error("PronounDB fetching failed: ", e); console.error("PronounDB fetching failed: ", e);
const dummyPronouns = Object.fromEntries(ids.map(id => [id, "unspecified"] as const)); const dummyPronouns = Object.fromEntries(ids.map(id => [id, { sets: {} }] as const));
Object.assign(cache, dummyPronouns); Object.assign(cache, dummyPronouns);
return dummyPronouns; return dummyPronouns;
} }
} }
export function formatPronouns(pronouns: string): string { export function extractPronouns(pronounSet?: { [locale: string]: PronounCode[] }): string {
if (!pronounSet || !pronounSet.en) return PronounMapping.unspecified;
// PronounDB returns an empty set instead of {sets: {en: ["unspecified"]}}.
const pronouns = pronounSet.en;
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; }; const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
// For capitalized pronouns, just return the mapping (it is by default capitalized)
if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns]; if (pronouns.length === 1) {
// If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text // For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string
else if ( if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]))
pronounsFormat === PronounsFormat.Lowercase return PronounMapping[pronouns[0]];
&& ["any", "ask", "avoid", "other"].includes(pronouns) else return PronounMapping[pronouns[0]].toLowerCase();
) return PronounMapping[pronouns]; }
// Otherwise (lowercase and not a special code), then convert the mapping to lowercase const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/");
else return PronounMapping[pronouns].toLowerCase(); return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase();
} }

View file

@ -26,31 +26,29 @@ export interface UserProfilePronounsProps {
} }
export interface PronounsResponse { export interface PronounsResponse {
[id: string]: PronounCode; [id: string]: {
sets?: {
[locale: string]: PronounCode[];
}
}
}
export interface CachePronouns {
sets?: {
[locale: string]: PronounCode[];
}
} }
export type PronounCode = keyof typeof PronounMapping; export type PronounCode = keyof typeof PronounMapping;
export const PronounMapping = { export const PronounMapping = {
hh: "He/Him", he: "He/Him",
hi: "He/It", it: "It/Its",
hs: "He/She", she: "She/Her",
ht: "He/They", they: "They/Them",
ih: "It/Him",
ii: "It/Its",
is: "It/She",
it: "It/They",
shh: "She/He",
sh: "She/Her",
si: "She/It",
st: "She/They",
th: "They/He",
ti: "They/It",
ts: "They/She",
tt: "They/Them",
any: "Any pronouns", any: "Any pronouns",
other: "Other pronouns", other: "Other pronouns",
ask: "Ask me my pronouns", ask: "Ask me my pronouns",
avoid: "Avoid pronouns, use my name", avoid: "Avoid pronouns, use my name",
unspecified: "Unspecified" unspecified: "No pronouns specified.",
} as const; } as const;

View file

@ -24,6 +24,7 @@ import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, PermissionsBi
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
const Kangaroo = findByPropsLazy("jumpToMessage"); const Kangaroo = findByPropsLazy("jumpToMessage");
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
const isMac = navigator.platform.includes("Mac"); // bruh const isMac = navigator.platform.includes("Mac"); // bruh
let replyIdx = -1; let replyIdx = -1;
@ -139,6 +140,10 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
messages = messages.filter(m => m.author.id === meId); messages = messages.filter(m => m.author.id === meId);
} }
if (Vencord.Plugins.isPluginEnabled("NoBlockedMessages")) {
messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
}
const mutate = (i: number) => isUp const mutate = (i: number) => isUp
? Math.min(messages.length - 1, i + 1) ? Math.min(messages.length - 1, i + 1)
: Math.max(-1, i - 1); : Math.max(-1, i - 1);

View file

@ -19,16 +19,37 @@
import "./style.css"; import "./style.css";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findStoreLazy } from "@webpack";
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common"; import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
import { Channel } from "discord-types/general";
interface ThreadJoined {
channel: Channel;
joinTimestamp: number;
}
type ThreadsJoined = Record<string, ThreadJoined>;
type ThreadsJoinedByParent = Record<string, ThreadsJoined>;
interface ActiveJoinedThreadsStore {
getActiveJoinedThreadsForGuild(guildId: string): ThreadsJoinedByParent;
}
const ActiveJoinedThreadsStore: ActiveJoinedThreadsStore = findStoreLazy("ActiveJoinedThreadsStore");
function onClick() { function onClick() {
const channels: Array<any> = []; const channels: Array<any> = [];
Object.values(GuildStore.getGuilds()).forEach(guild => { Object.values(GuildStore.getGuilds()).forEach(guild => {
GuildChannelStore.getChannels(guild.id).SELECTABLE GuildChannelStore.getChannels(guild.id).SELECTABLE // Array<{ channel, comparator }>
.concat(GuildChannelStore.getChannels(guild.id).VOCAL) .concat(GuildChannelStore.getChannels(guild.id).VOCAL) // Array<{ channel, comparator }>
.concat(
Object.values(ActiveJoinedThreadsStore.getActiveJoinedThreadsForGuild(guild.id))
.flatMap(threadChannels => Object.values(threadChannels))
)
.forEach((c: { channel: { id: string; }; }) => { .forEach((c: { channel: { id: string; }; }) => {
if (!ReadStateStore.hasUnread(c.channel.id)) return; if (!ReadStateStore.hasUnread(c.channel.id)) return;
@ -64,7 +85,7 @@ export default definePlugin({
authors: [Devs.kemo], authors: [Devs.kemo],
dependencies: ["ServerListAPI"], dependencies: ["ServerListAPI"],
renderReadAllButton: () => <ReadAllButton />, renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),
start() { start() {
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton); addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);

View file

@ -0,0 +1,5 @@
# ReplaceGoogleSearch
Replaces the Google search with different Engines
![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/8b8158d2-0407-4d7b-9dff-a8b9bdc1a122)

View file

@ -0,0 +1,107 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Flex, Menu } from "@webpack/common";
const DefaultEngines = {
Google: "https://www.google.com/search?q=",
DuckDuckGo: "https://duckduckgo.com/",
Bing: "https://www.bing.com/search?q=",
Yahoo: "https://search.yahoo.com/search?p=",
GitHub: "https://github.com/search?q=",
Kagi: "https://kagi.com/search?q=",
Yandex: "https://yandex.com/search/?text=",
AOL: "https://search.aol.com/aol/search?q=",
Baidu: "https://www.baidu.com/s?wd=",
Wikipedia: "https://wikipedia.org/w/index.php?search=",
} as const;
const settings = definePluginSettings({
customEngineName: {
description: "Name of the custom search engine",
type: OptionType.STRING,
placeholder: "Google"
},
customEngineURL: {
description: "The URL of your Engine",
type: OptionType.STRING,
placeholder: "https://google.com/search?q="
}
});
function search(src: string, engine: string) {
open(engine + encodeURIComponent(src), "_blank");
}
function makeSearchItem(src: string) {
let Engines = {};
if (settings.store.customEngineName && settings.store.customEngineURL) {
Engines[settings.store.customEngineName] = settings.store.customEngineURL;
}
Engines = { ...Engines, ...DefaultEngines };
return (
<Menu.MenuItem
label="Search Text"
key="search-text"
id="vc-search-text"
>
{Object.keys(Engines).map((engine, i) => {
const key = "vc-search-content-" + engine;
return (
<Menu.MenuItem
key={key}
id={key}
label={
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
<img
style={{
borderRadius: "50%"
}}
aria-hidden="true"
height={16}
width={16}
src={`https://www.google.com/s2/favicons?domain=${Engines[engine]}`}
/>
{engine}
</Flex>
}
action={() => search(src, Engines[engine])}
/>
);
})}
</Menu.MenuItem>
);
}
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, _props) => {
const selection = document.getSelection()?.toString();
if (!selection) return;
const group = findGroupChildrenByChildId("search-google", children);
if (group) {
const idx = group.findIndex(c => c?.props?.id === "search-google");
if (idx !== -1) group[idx] = makeSearchItem(selection);
}
};
export default definePlugin({
name: "ReplaceGoogleSearch",
description: "Replaces the Google search with different Engines",
authors: [Devs.Moxxie, Devs.Ethan],
settings,
contextMenus: {
"message": messageContextMenuPatch
}
});

View file

@ -134,8 +134,8 @@ export default definePlugin({
{ {
find: '"MessageActionCreators"', find: '"MessageActionCreators"',
replacement: { replacement: {
match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/, match: /focusMessage\(\i\){.+?(?=focus:{messageId:(\i)})/,
replace: "before:$1," replace: "$&after:$1,"
} }
}, },
// Force Server Home instead of Server Guide // Force Server Home instead of Server Guide

View file

@ -20,7 +20,7 @@ import "./style.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader"; import { ExpandableHeader } from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons"; import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";

View file

@ -89,8 +89,8 @@ export default definePlugin({
}, },
// Remove permission checking for getRenderLevel function // Remove permission checking for getRenderLevel function
{ {
match: /(?<=getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/, match: /(getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/,
replace: " " replace: (_, rest) => `${rest} `
} }
] ]
}, },
@ -159,8 +159,8 @@ export default definePlugin({
replacement: [ replacement: [
// Make the channel appear as muted if it's hidden // Make the channel appear as muted if it's hidden
{ {
match: /(?<={channel:(\i),name:\i,muted:(\i).+?;)/, match: /{channel:(\i),name:\i,muted:(\i).+?;/,
replace: (_, channel, muted) => `${muted}=$self.isHiddenChannel(${channel})?true:${muted};` replace: (m, channel, muted) => `${m}${muted}=$self.isHiddenChannel(${channel})?true:${muted};`
}, },
// Add the hidden eye icon if the channel is hidden // Add the hidden eye icon if the channel is hidden
{ {
@ -186,8 +186,8 @@ export default definePlugin({
{ {
// Hide unreads // Hide unreads
predicate: () => settings.store.hideUnreads === true, predicate: () => settings.store.hideUnreads === true,
match: /(?<={channel:(\i),name:\i,.+?unread:(\i).+?;)/, match: /{channel:(\i),name:\i,.+?unread:(\i).+?;/,
replace: (_, channel, unread) => `${unread}=$self.isHiddenChannel(${channel})?false:${unread};` replace: (m, channel, unread) => `${m}${unread}=$self.isHiddenChannel(${channel})?false:${unread};`
} }
] ]
}, },
@ -436,7 +436,7 @@ export default definePlugin({
}, },
}, },
{ {
find: ".shouldCloseDefaultModals", find: 'className:"channelMention",children',
replacement: { replacement: {
// Show inside voice channel instead of trying to join them when clicking on a channel mention // Show inside voice channel instead of trying to join them when clicking on a channel mention
match: /(?<=getChannel\(\i\);if\(null!=(\i))(?=.{0,100}?selectVoiceChannel)/, match: /(?<=getChannel\(\i\);if\(null!=(\i))(?=.{0,100}?selectVoiceChannel)/,

View file

@ -1,6 +1,6 @@
# ShowHiddenThings # ShowHiddenThings
Displays various moderator-only elements regardless of permissions. Displays various hidden & moderator-only things regardless of permissions.
## Features ## Features
@ -15,3 +15,5 @@ Displays various moderator-only elements regardless of permissions.
![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab) ![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab)
- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria - Disable filters in Server Discovery search that hide servers that don't meet discovery criteria
- Disable filters in Server Discovery search that hide NSFW & disallowed servers

View file

@ -41,13 +41,18 @@ const settings = definePluginSettings({
description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.", description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.",
default: true, default: true,
}, },
disableDisallowedDiscoveryFilters: {
type: OptionType.BOOLEAN,
description: "Disable filters in Server Discovery search that hide NSFW & disallowed servers.",
default: true,
},
}); });
migratePluginSettings("ShowHiddenThings", "ShowTimeouts"); migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
export default definePlugin({ export default definePlugin({
name: "ShowHiddenThings", name: "ShowHiddenThings",
tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"], tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
description: "Displays various moderator-only elements regardless of permissions.", description: "Displays various hidden & moderator-only things regardless of permissions.",
authors: [Devs.Dolfies], authors: [Devs.Dolfies],
patches: [ patches: [
{ {
@ -75,11 +80,36 @@ export default definePlugin({
} }
}, },
{ {
find: "auto_removed:", find: "prod_discoverable_guilds",
predicate: () => settings.store.disableDiscoveryFilters, predicate: () => settings.store.disableDiscoveryFilters,
replacement: { replacement: {
match: /filters:\i\.join\(" AND "\),facets:\[/, match: /\{"auto_removed:.*?\}/,
replace: "facets:[" replace: "{}"
}
},
{
find: "MINIMUM_MEMBER_COUNT:",
predicate: () => settings.store.disableDiscoveryFilters,
replacement: {
match: /MINIMUM_MEMBER_COUNT:function\(\)\{return \i}/,
replace: "MINIMUM_MEMBER_COUNT:() => \">0\""
}
},
{
find: "DiscoveryBannedSearchWords.includes",
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
replacement: {
match: /(?<=function\(\){)(?=.{0,130}DiscoveryBannedSearchWords\.includes)/,
replace: "return false;"
}
},
{
find: "Endpoints.GUILD_DISCOVERY_VALID_TERM",
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
all: true,
replacement: {
match: /\i\.HTTP\.get\(\{url:\i\.Endpoints\.GUILD_DISCOVERY_VALID_TERM,query:\{term:\i\},oldFormErrors:!0\}\);/g,
replace: "Promise.resolve({ body: { valid: true } });"
} }
} }
], ],

View file

@ -7,6 +7,7 @@
import "./styles.css"; import "./styles.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Message, User } from "discord-types/general"; import { Message, User } from "discord-types/general";
@ -56,7 +57,7 @@ export default definePlugin({
], ],
settings, settings,
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => { renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
try { try {
const user = userOverride ?? message.author; const user = userOverride ?? message.author;
let { username } = user; let { username } = user;
@ -66,14 +67,14 @@ export default definePlugin({
const { nick } = author; const { nick } = author;
const prefix = withMentionPrefix ? "@" : ""; const prefix = withMentionPrefix ? "@" : "";
if (username === nick || isRepliedMessage && !settings.store.inReplies) if (username === nick || isRepliedMessage && !settings.store.inReplies)
return prefix + nick; return <>{prefix}{nick}</>;
if (settings.store.mode === "user-nick") if (settings.store.mode === "user-nick")
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>; return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
if (settings.store.mode === "nick-user") if (settings.store.mode === "nick-user")
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>; return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
return prefix + username; return <>{prefix}{username}</>;
} catch { } catch {
return author?.nick; return <>{author?.nick}</>;
} }
}, }, { noop: true }),
}); });

View file

@ -0,0 +1,8 @@
# ShowTimeoutDuration
Displays how much longer a user's timeout will last.
Either in the timeout icon tooltip, or next to it, configurable via settings!
![indicator in tooltip](https://github.com/Vendicated/Vencord/assets/45497981/606588a3-2646-40d9-8800-b6307f650136)
![indicator next to timeout icon](https://github.com/Vendicated/Vencord/assets/45497981/ab9d2101-0fdc-4143-9310-9488f056eeee)

View file

@ -0,0 +1,92 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentLazy } from "@webpack";
import { ChannelStore, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
import { FunctionComponent, ReactNode } from "react";
const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER"));
const enum DisplayStyle {
Tooltip = "tooltip",
Inline = "ssalggnikool"
}
const settings = definePluginSettings({
displayStyle: {
description: "How to display the timeout duration",
type: OptionType.SELECT,
options: [
{ label: "In the Tooltip", value: DisplayStyle.Tooltip },
{ label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true },
],
}
});
function renderTimeout(message: Message, inline: boolean) {
const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id;
if (!guildId) return null;
const member = GuildMemberStore.getMember(guildId, message.author.id);
if (!member?.communicationDisabledUntil) return null;
const countdown = () => (
<CountDown
deadline={new Date(member.communicationDisabledUntil!)}
showUnits
stopAtOneSec
/>
);
return inline
? countdown()
: i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({
username: message.author.username,
countdown
});
}
export default definePlugin({
name: "ShowTimeoutDuration",
description: "Shows how much longer a user's timeout will last, either in the timeout icon tooltip or next to it",
authors: [Devs.Ven, Devs.Sqaaakoi],
settings,
patches: [
{
find: ".GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY",
replacement: [
{
match: /(\i)\.Tooltip,{(text:.{0,30}\.Messages\.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY)/,
replace: "$self.TooltipWrapper,{message:arguments[0].message,$2"
}
]
}
],
TooltipWrapper: ErrorBoundary.wrap(({ message, children, text }: { message: Message; children: FunctionComponent<any>; text: ReactNode; }) => {
if (settings.store.displayStyle === DisplayStyle.Tooltip) return <Tooltip
children={children}
text={renderTimeout(message, false)}
/>;
return (
<div className="vc-std-wrapper">
<Tooltip text={text} children={children} />
<Text variant="text-md/normal" color="status-danger">
{renderTimeout(message, true)} timeout remaining
</Text>
</div>
);
}, { noop: true })
});

View file

@ -0,0 +1,8 @@
.vc-std-wrapper {
display: flex;
align-items: center;
}
.vc-std-wrapper [class*="communicationDisabled"] {
margin-right: 0;
}

View file

@ -18,10 +18,11 @@
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, React } from "@webpack/common"; import { FluxDispatcher, Menu, React } from "@webpack/common";
const settings = definePluginSettings({ const settings = definePluginSettings({
showIcon: { showIcon: {
@ -30,6 +31,11 @@ const settings = definePluginSettings({
description: "Show an icon for toggling the plugin", description: "Show an icon for toggling the plugin",
restartNeeded: true, restartNeeded: true,
}, },
contextMenu: {
type: OptionType.BOOLEAN,
description: "Add option to toggle the functionality in the chat input context menu",
default: true
},
isEnabled: { isEnabled: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Toggle functionality", description: "Toggle functionality",
@ -56,13 +62,37 @@ const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
); );
}; };
const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {
const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]);
if (!contextMenu) return;
const group = findGroupChildrenByChildId("submit-button", children);
if (!group) return;
const idx = group.findIndex(c => c?.props?.id === "submit-button");
group.splice(idx + 1, 0,
<Menu.MenuCheckboxItem
id="vc-silent-typing"
label="Enable Silent Typing"
checked={isEnabled}
action={() => settings.store.isEnabled = !settings.store.isEnabled}
/>
);
};
export default definePlugin({ export default definePlugin({
name: "SilentTyping", name: "SilentTyping",
authors: [Devs.Ven, Devs.Rini], authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
description: "Hide that you are typing", description: "Hide that you are typing",
dependencies: ["CommandsAPI", "ChatInputButtonAPI"], dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
settings, settings,
contextMenus: {
"textarea-context": ChatBarContextCheckbox
},
patches: [ patches: [
{ {
find: '.dispatch({type:"TYPING_START_LOCAL"', find: '.dispatch({type:"TYPING_START_LOCAL"',

View file

@ -60,8 +60,8 @@ export default definePlugin({
}, },
{ {
predicate: () => settings.store.keepSpotifyActivityOnIdle, predicate: () => settings.store.keepSpotifyActivityOnIdle,
match: /(?<=shouldShowActivity\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/, match: /(shouldShowActivity\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/,
replace: "" replace: "$1"
} }
] ]
} }

View file

@ -26,10 +26,12 @@ export default definePlugin({
description: "Adds Startup Timings to the Settings menu", description: "Adds Startup Timings to the Settings menu",
authors: [Devs.Megu], authors: [Devs.Megu],
patches: [{ patches: [{
find: "UserSettingsSections.PAYMENT_FLOW_MODAL_TEST_PAGE,", find: "Messages.ACTIVITY_SETTINGS",
replacement: { replacement: {
match: /{section:\i\.UserSettingsSections\.PAYMENT_FLOW_MODAL_TEST_PAGE/, match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\)}\))/,
replace: '{section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage},$&' replace: (_, commaOrSemi, settings, elements) => "" +
`${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
`&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`
} }
}], }],
StartupTimingPage StartupTimingPage

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