1
0
Fork 1
mirror of https://github.com/Vendicated/Vencord.git synced 2025-01-09 17:36:23 +00:00

Merge branch 'Vendicated:main' into TablistApi

This commit is contained in:
programminglaboratorys 2024-10-05 08:04:03 +03:00 committed by GitHub
commit 8c6a99f75d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
209 changed files with 5870 additions and 3737 deletions

View file

@ -1,98 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "browser", "packages/vencord-types"],
"plugins": [
"@typescript-eslint",
"simple-header",
"simple-import-sort",
"unused-imports",
"path-alias"
],
"settings": {
"import/resolver": {
"alias": {
"map": [
["@webpack", "./src/webpack"],
["@webpack/common", "./src/webpack/common"],
["@utils", "./src/utils"],
["@api", "./src/api"],
["@components", "./src/components"]
]
}
}
},
"rules": {
// Since it's only been a month and Vencord has already been stolen
// by random skids who rebranded it to "AlphaCord" and erased all license
// information
"simple-header/header": [
"error",
{
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
"templates": { "author": [".*", "Vendicated and contributors"] }
}
],
"quotes": ["error", "double", { "avoidEscape": true }],
"jsx-quotes": ["error", "prefer-double"],
"no-mixed-spaces-and-tabs": "error",
"indent": ["error", 4, { "SwitchCase": 1 }],
"arrow-parens": ["error", "as-needed"],
"eol-last": ["error", "always"],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"no-multi-spaces": "error",
"no-trailing-spaces": "error",
"no-whitespace-before-property": "error",
"semi": ["error", "always"],
"semi-style": ["error", "last"],
"space-in-parens": ["error", "never"],
"block-spacing": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"spaced-comment": ["error", "always", { "markers": ["!"] }],
"yoda": "error",
"prefer-destructuring": ["error", {
"VariableDeclarator": { "array": false, "object": true },
"AssignmentExpression": { "array": false, "object": false }
}],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"dot-notation": "error",
"no-useless-escape": [
"error",
{
"extra": "i"
}
],
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"path-alias/no-relative": "error"
}
}

View file

@ -1,6 +1,11 @@
{ {
"extends": "stylelint-config-standard", "extends": "stylelint-config-standard",
"rules": { "rules": {
"indentation": 4 "selector-class-pattern": [
"^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$",
{
"message": "Expected class selector to be kebab-case with camelCase segments"
}
]
} }
} }

View file

@ -14,8 +14,6 @@
"typescript.preferences.quoteStyle": "double", "typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double", "javascript.preferences.quoteStyle": "double",
"eslint.experimental.useFlatConfig": false,
"gitlens.remotes": [ "gitlens.remotes": [
{ {
"domain": "codeberg.org", "domain": "codeberg.org",

View file

@ -1,6 +1,6 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"minimum_chrome_version": "91", "minimum_chrome_version": "111",
"name": "Vencord Web", "name": "Vencord Web",
"description": "The cutest Discord mod now in your browser", "description": "The cutest Discord mod now in your browser",

View file

@ -43,7 +43,7 @@
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "vencord-firefox@vendicated.dev", "id": "vencord-firefox@vendicated.dev",
"strict_min_version": "91.0" "strict_min_version": "128.0"
} }
} }
} }

126
eslint.config.mjs Normal file
View file

@ -0,0 +1,126 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// @ts-check
import stylistic from "@stylistic/eslint-plugin";
import pathAlias from "eslint-plugin-path-alias";
import header from "eslint-plugin-simple-header";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist", "browser", "packages/vencord-types"] },
{
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"],
plugins: {
"simple-header": header,
"@stylistic": stylistic,
"@typescript-eslint": tseslint.plugin,
"simple-import-sort": simpleImportSort,
"unused-imports": unusedImports,
"path-alias": pathAlias,
},
settings: {
"import/resolver": {
map: [
["@webpack", "./src/webpack"],
["@webpack/common", "./src/webpack/common"],
["@utils", "./src/utils"],
["@api", "./src/api"],
["@components", "./src/components"]
]
}
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: ["./tsconfig.json"],
tsconfigRootDir: import.meta.dirname
}
},
rules: {
/*
* Since it's only been a month and Vencord has already been stolen
* by random skids who rebranded it to "AlphaCord" and erased all license
* information
*/
"simple-header/header": [
"error",
{
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
"templates": { "author": [".*", "Vendicated and contributors"] }
}
],
// Style Rules
"@stylistic/jsx-quotes": ["error", "prefer-double"],
"@stylistic/quotes": ["error", "double", { "avoidEscape": true }],
"@stylistic/no-mixed-spaces-and-tabs": "error",
"@stylistic/arrow-parens": ["error", "as-needed"],
"@stylistic/eol-last": ["error", "always"],
"@stylistic/no-multi-spaces": "error",
"@stylistic/no-trailing-spaces": "error",
"@stylistic/no-whitespace-before-property": "error",
"@stylistic/semi": ["error", "always"],
"@stylistic/semi-style": ["error", "last"],
"@stylistic/space-in-parens": ["error", "never"],
"@stylistic/block-spacing": ["error", "always"],
"@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/spaced-comment": ["error", "always", { "markers": ["!"] }],
"@stylistic/no-extra-semi": "error",
// TS Rules
"@stylistic/func-call-spacing": ["error", "never"],
// ESLint Rules
"yoda": "error",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"prefer-destructuring": ["error", {
"VariableDeclarator": { "array": false, "object": true },
"AssignmentExpression": { "array": false, "object": false }
}],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"dot-notation": "error",
"no-useless-escape": [
"error",
{
"extra": "i"
}
],
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
// Plugin Rules
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"path-alias/no-relative": "error"
}
}
);

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.9.3", "version": "1.10.3",
"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": {
@ -21,12 +21,13 @@
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension", "buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
"buildReporterDesktop": "pnpm build --reporter", "buildReporterDesktop": "pnpm build --reporter",
"watch": "pnpm build --watch", "watch": "pnpm build --watch",
"dev": "pnpm watch",
"watchWeb": "pnpm buildWeb --watch", "watchWeb": "pnpm buildWeb --watch",
"generatePluginJson": "tsx scripts/generatePluginList.ts", "generatePluginJson": "tsx scripts/generatePluginList.ts",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types", "generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "node scripts/runInstaller.mjs", "inject": "node scripts/runInstaller.mjs",
"uninject": "node scripts/runInstaller.mjs", "uninject": "node scripts/runInstaller.mjs",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins", "lint": "eslint",
"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 buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson", "test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
@ -34,53 +35,54 @@
"testTsc": "tsc --noEmit" "testTsc": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@sapphi-red/web-noise-suppressor": "0.3.3", "@sapphi-red/web-noise-suppressor": "0.3.5",
"@vap/core": "0.0.12", "@vap/core": "0.0.12",
"@vap/shiki": "0.10.5", "@vap/shiki": "0.10.5",
"eslint-plugin-simple-header": "^1.0.2", "fflate": "^0.8.2",
"fflate": "^0.7.4",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.50.0", "monaco-editor": "^0.50.0",
"nanoid": "^4.0.2", "nanoid": "^5.0.7",
"virtual-merge": "^1.0.1" "virtual-merge": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/chrome": "^0.0.246", "@stylistic/eslint-plugin": "^2.6.1",
"@types/diff": "^5.0.3", "@types/chrome": "^0.0.269",
"@types/lodash": "^4.14.194", "@types/diff": "^5.2.1",
"@types/node": "^18.16.3", "@types/lodash": "^4.17.7",
"@types/react": "^18.2.0", "@types/node": "^22.0.3",
"@types/react-dom": "^18.2.1", "@types/react": "^18.3.3",
"@types/yazl": "^2.4.2", "@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^5.59.1", "@types/yazl": "^2.4.5",
"@typescript-eslint/parser": "^5.59.1", "diff": "^5.2.0",
"diff": "^5.1.0",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
"esbuild": "^0.15.18", "esbuild": "^0.15.18",
"eslint": "^8.46.0", "eslint": "^9.8.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "^1.0.0", "eslint-plugin-path-alias": "2.1.0",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-header": "^1.1.1",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-simple-import-sort": "^12.1.1",
"highlight.js": "10.6.0", "eslint-plugin-unused-imports": "^4.0.1",
"highlight.js": "10.7.3",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"moment": "^2.29.4", "moment": "^2.30.1",
"puppeteer-core": "^19.11.1", "puppeteer-core": "^22.15.0",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",
"stylelint": "^15.6.0", "stylelint": "^16.8.1",
"stylelint-config-standard": "^33.0.0", "stylelint-config-standard": "^36.0.1",
"ts-patch": "^3.1.2", "ts-patch": "^3.2.1",
"tsx": "^3.12.7", "ts-pattern": "^5.3.1",
"type-fest": "^3.9.0", "tsx": "^4.16.5",
"typescript": "^5.4.5", "type-fest": "^4.23.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.0",
"typescript-transform-paths": "^3.4.7", "typescript-transform-paths": "^3.4.7",
"zip-local": "^0.3.5" "zip-local": "^0.3.5"
}, },
"packageManager": "pnpm@9.1.0", "packageManager": "pnpm@9.1.0",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch", "eslint@9.8.0": "patches/eslint@9.8.0.patch",
"eslint@8.46.0": "patches/eslint@8.46.0.patch" "eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch"
}, },
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [ "ignoreMissing": [

View file

@ -1,13 +0,0 @@
diff --git a/lib/rules/no-relative.js b/lib/rules/no-relative.js
index 71594c83f1f4f733ffcc6047d7f7084348335dbe..d8623d87c89499c442171db3272cba07c9efabbe 100644
--- a/lib/rules/no-relative.js
+++ b/lib/rules/no-relative.js
@@ -41,7 +41,7 @@ module.exports = {
ImportDeclaration(node) {
const importPath = node.source.value;
- if (!/^(\.?\.\/)/.test(importPath)) {
+ if (!/^(\.\.\/)/.test(importPath)) {
return;
}

View file

@ -0,0 +1,14 @@
diff --git a/dist/index.js b/dist/index.js
index 67de6fb139070fd0e49beca65e3b63c531202e16..aa2883c8126e4952a42872ee920f59547a066430 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1 +1 @@
-var C=Object.create;var f=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var $=(e,t)=>{for(var r in t)f(e,r,{get:t[r],enumerable:!0})},y=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of U(t))!F.call(e,s)&&s!==r&&f(e,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return e};var b=(e,t,r)=>(r=e!=null?C(S(e)):{},y(t||!e||!e.__esModule?f(r,"default",{value:e,enumerable:!0}):r,e)),D=e=>y(f({},"__esModule",{value:!0}),e);var N={};$(N,{default:()=>J});module.exports=D(N);var h="eslint-plugin-path-alias",v="2.0.0";var l=require("path"),M=b(require("nanomatch"));function j(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}var R=require("get-tsconfig"),a=require("path"),w=b(require("find-pkg")),O=require("fs");function P(e){if(e.options[0]?.paths)return z(e);let t=e.getFilename?.()??e.filename,r=(0,R.getTsconfig)(t);if(r?.config?.compilerOptions?.paths)return q(r);let i=w.default.sync((0,a.dirname)(t));if(!i)return;let s=JSON.parse((0,O.readFileSync)(i).toString());if(s?.imports)return L(s,i)}function L(e,t){let r=new Map,i=e.imports??{},s=(0,a.dirname)(t);return Object.entries(i).forEach(([o,n])=>{if(!n||typeof n!="string")return;let p=(0,a.resolve)(s,n);r.set(o,[p])}),r}function q(e){let t=new Map,r=e?.config?.compilerOptions?.paths??{},i=(0,a.dirname)(e.path);return e.config.compilerOptions?.baseUrl&&(i=(0,a.resolve)((0,a.dirname)(e.path),e.config.compilerOptions.baseUrl)),Object.entries(r).forEach(([s,o])=>{s=s.replace(/\/\*$/,""),o=o.map(n=>(0,a.resolve)(i,n.replace(/\/\*$/,""))),t.set(s,o)}),t}function z(e){let t=new Map,r=e.options[0]?.paths??{};return Object.entries(r).forEach(([i,s])=>{if(!s||typeof s!="string")return;if(s.startsWith("/")){t.set(i,[s]);return}let o=e.getCwd?.()??e.cwd,n=(0,a.resolve)(o,s);t.set(i,[n])}),t}var T={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:j("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let t=e.options[0]?.exceptions,r=e.getFilename?.()??e.filename,i=P(e);return i?.size?{ImportExpression(s){if(s.source.type!=="Literal"||typeof s.source.value!="string")return;let o=s.source.raw,n=s.source.value;if(!/^(\.?\.\/)/.test(n))return;let p=(0,l.resolve)((0,l.dirname)(r),n);if(A(p,t))return;let c=k(p,i);c&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:c},fix(m){let g=E(p,c,i.get(c)),d=o.replace(n,g);return m.replaceText(s.source,d)}})},ImportDeclaration(s){if(typeof s.source.value!="string")return;let o=s.source.value;if(!/^(\.?\.\/)/.test(o))return;let n=(0,l.resolve)((0,l.dirname)(r),o),p=A(n,t),u=k(n,i);p||u&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:u},fix(c){let m=s.source.raw,g=E(n,u,i.get(u)),d=m.replace(o,g);return c.replaceText(s.source,d)}})}}:{}}};function k(e,t){return Array.from(t.keys()).find(r=>t.get(r).some(s=>e.indexOf(s)===0))}function A(e,t){if(!t)return!1;let r=(0,l.basename)(e);return(0,M.default)(r,t).includes(r)}function E(e,t,r){for(let i of r)if(e.indexOf(i)===0)return e.replace(i,t)}var J={name:h,version:v,meta:{name:h,version:v},rules:{"no-relative":T}};
+var C=Object.create;var f=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var $=(e,t)=>{for(var r in t)f(e,r,{get:t[r],enumerable:!0})},y=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of U(t))!F.call(e,s)&&s!==r&&f(e,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return e};var b=(e,t,r)=>(r=e!=null?C(S(e)):{},y(t||!e||!e.__esModule?f(r,"default",{value:e,enumerable:!0}):r,e)),D=e=>y(f({},"__esModule",{value:!0}),e);var N={};$(N,{default:()=>J});module.exports=D(N);var h="eslint-plugin-path-alias",v="2.0.0";var l=require("path"),M=b(require("nanomatch"));function j(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}var R=require("get-tsconfig"),a=require("path"),w=b(require("find-pkg")),O=require("fs");function P(e){if(e.options[0]?.paths)return z(e);let t=e.getFilename?.()??e.filename,r=(0,R.getTsconfig)(t);if(r?.config?.compilerOptions?.paths)return q(r);let i=w.default.sync((0,a.dirname)(t));if(!i)return;let s=JSON.parse((0,O.readFileSync)(i).toString());if(s?.imports)return L(s,i)}function L(e,t){let r=new Map,i=e.imports??{},s=(0,a.dirname)(t);return Object.entries(i).forEach(([o,n])=>{if(!n||typeof n!="string")return;let p=(0,a.resolve)(s,n);r.set(o,[p])}),r}function q(e){let t=new Map,r=e?.config?.compilerOptions?.paths??{},i=(0,a.dirname)(e.path);return e.config.compilerOptions?.baseUrl&&(i=(0,a.resolve)((0,a.dirname)(e.path),e.config.compilerOptions.baseUrl)),Object.entries(r).forEach(([s,o])=>{s=s.replace(/\/\*$/,""),o=o.map(n=>(0,a.resolve)(i,n.replace(/\/\*$/,""))),t.set(s,o)}),t}function z(e){let t=new Map,r=e.options[0]?.paths??{};return Object.entries(r).forEach(([i,s])=>{if(!s||typeof s!="string")return;if(s.startsWith("/")){t.set(i,[s]);return}let o=e.getCwd?.()??e.cwd,n=(0,a.resolve)(o,s);t.set(i,[n])}),t}var T={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:j("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let t=e.options[0]?.exceptions,r=e.getFilename?.()??e.filename,i=P(e);return i?.size?{ImportExpression(s){if(s.source.type!=="Literal"||typeof s.source.value!="string")return;let o=s.source.raw,n=s.source.value;if(!/^(\.\.\/)/.test(n))return;let p=(0,l.resolve)((0,l.dirname)(r),n);if(A(p,t))return;let c=k(p,i);c&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:c},fix(m){let g=E(p,c,i.get(c)),d=o.replace(n,g);return m.replaceText(s.source,d)}})},ImportDeclaration(s){if(typeof s.source.value!="string")return;let o=s.source.value;if(!/^(\.\.\/)/.test(o))return;let n=(0,l.resolve)((0,l.dirname)(r),o),p=A(n,t),u=k(n,i);p||u&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:u},fix(c){let m=s.source.raw,g=E(n,u,i.get(u)),d=m.replace(o,g);return c.replaceText(s.source,d)}})}}:{}}};function k(e,t){return Array.from(t.keys()).find(r=>t.get(r).some(s=>e.indexOf(s)===0))}function A(e,t){if(!t)return!1;let r=(0,l.basename)(e);return(0,M.default)(r,t).includes(r)}function E(e,t,r){for(let i of r)if(e.indexOf(i)===0)return e.replace(i,t)}var J={name:h,version:v,meta:{name:h,version:v},rules:{"no-relative":T}};
diff --git a/dist/index.mjs b/dist/index.mjs
index 96de18e06d4cc413e11af038cd760e4804c32e59..27e8c4e3e2c942400cc3982e52159904ca6eedfa 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1 +1 @@
-var d="eslint-plugin-path-alias",h="2.0.0";import{dirname as x,resolve as j,basename as I}from"path";import U from"nanomatch";function y(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}import{getTsconfig as k}from"get-tsconfig";import{resolve as c,dirname as u}from"path";import A from"find-pkg";import{readFileSync as E}from"fs";function b(e){if(e.options[0]?.paths)return C(e);let s=e.getFilename?.()??e.filename,i=k(s);if(i?.config?.compilerOptions?.paths)return T(i);let r=A.sync(u(s));if(!r)return;let t=JSON.parse(E(r).toString());if(t?.imports)return M(t,r)}function M(e,s){let i=new Map,r=e.imports??{},t=u(s);return Object.entries(r).forEach(([o,n])=>{if(!n||typeof n!="string")return;let a=c(t,n);i.set(o,[a])}),i}function T(e){let s=new Map,i=e?.config?.compilerOptions?.paths??{},r=u(e.path);return e.config.compilerOptions?.baseUrl&&(r=c(u(e.path),e.config.compilerOptions.baseUrl)),Object.entries(i).forEach(([t,o])=>{t=t.replace(/\/\*$/,""),o=o.map(n=>c(r,n.replace(/\/\*$/,""))),s.set(t,o)}),s}function C(e){let s=new Map,i=e.options[0]?.paths??{};return Object.entries(i).forEach(([r,t])=>{if(!t||typeof t!="string")return;if(t.startsWith("/")){s.set(r,[t]);return}let o=e.getCwd?.()??e.cwd,n=c(o,t);s.set(r,[n])}),s}var P={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:y("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let s=e.options[0]?.exceptions,i=e.getFilename?.()??e.filename,r=b(e);return r?.size?{ImportExpression(t){if(t.source.type!=="Literal"||typeof t.source.value!="string")return;let o=t.source.raw,n=t.source.value;if(!/^(\.?\.\/)/.test(n))return;let a=j(x(i),n);if(w(a,s))return;let l=R(a,r);l&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:l},fix(f){let m=O(a,l,r.get(l)),g=o.replace(n,m);return f.replaceText(t.source,g)}})},ImportDeclaration(t){if(typeof t.source.value!="string")return;let o=t.source.value;if(!/^(\.?\.\/)/.test(o))return;let n=j(x(i),o),a=w(n,s),p=R(n,r);a||p&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:p},fix(l){let f=t.source.raw,m=O(n,p,r.get(p)),g=f.replace(o,m);return l.replaceText(t.source,g)}})}}:{}}};function R(e,s){return Array.from(s.keys()).find(i=>s.get(i).some(t=>e.indexOf(t)===0))}function w(e,s){if(!s)return!1;let i=I(e);return U(i,s).includes(i)}function O(e,s,i){for(let r of i)if(e.indexOf(r)===0)return e.replace(r,s)}var Q={name:d,version:h,meta:{name:d,version:h},rules:{"no-relative":P}};export{Q as default};
+var d="eslint-plugin-path-alias",h="2.0.0";import{dirname as x,resolve as j,basename as I}from"path";import U from"nanomatch";function y(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}import{getTsconfig as k}from"get-tsconfig";import{resolve as c,dirname as u}from"path";import A from"find-pkg";import{readFileSync as E}from"fs";function b(e){if(e.options[0]?.paths)return C(e);let s=e.getFilename?.()??e.filename,i=k(s);if(i?.config?.compilerOptions?.paths)return T(i);let r=A.sync(u(s));if(!r)return;let t=JSON.parse(E(r).toString());if(t?.imports)return M(t,r)}function M(e,s){let i=new Map,r=e.imports??{},t=u(s);return Object.entries(r).forEach(([o,n])=>{if(!n||typeof n!="string")return;let a=c(t,n);i.set(o,[a])}),i}function T(e){let s=new Map,i=e?.config?.compilerOptions?.paths??{},r=u(e.path);return e.config.compilerOptions?.baseUrl&&(r=c(u(e.path),e.config.compilerOptions.baseUrl)),Object.entries(i).forEach(([t,o])=>{t=t.replace(/\/\*$/,""),o=o.map(n=>c(r,n.replace(/\/\*$/,""))),s.set(t,o)}),s}function C(e){let s=new Map,i=e.options[0]?.paths??{};return Object.entries(i).forEach(([r,t])=>{if(!t||typeof t!="string")return;if(t.startsWith("/")){s.set(r,[t]);return}let o=e.getCwd?.()??e.cwd,n=c(o,t);s.set(r,[n])}),s}var P={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:y("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let s=e.options[0]?.exceptions,i=e.getFilename?.()??e.filename,r=b(e);return r?.size?{ImportExpression(t){if(t.source.type!=="Literal"||typeof t.source.value!="string")return;let o=t.source.raw,n=t.source.value;if(!/^(\.\.\/)/.test(n))return;let a=j(x(i),n);if(w(a,s))return;let l=R(a,r);l&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:l},fix(f){let m=O(a,l,r.get(l)),g=o.replace(n,m);return f.replaceText(t.source,g)}})},ImportDeclaration(t){if(typeof t.source.value!="string")return;let o=t.source.value;if(!/^(\.\.\/)/.test(o))return;let n=j(x(i),o),a=w(n,s),p=R(n,r);a||p&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:p},fix(l){let f=t.source.raw,m=O(n,p,r.get(p)),g=f.replace(o,m);return l.replaceText(t.source,g)}})}}:{}}};function R(e,s){return Array.from(s.keys()).find(i=>s.get(i).some(t=>e.indexOf(t)===0))}function w(e,s){if(!s)return!1;let i=I(e);return U(i,s).includes(i)}function O(e,s,i){for(let r of i)if(e.indexOf(r)===0)return e.replace(r,s)}var Q={name:d,version:h,meta:{name:d,version:h},rules:{"no-relative":P}};export{Q as default};

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -36,7 +36,7 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
const CANARY = process.env.USE_CANARY === "true"; const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({ const browser = await pup.launch({
headless: "new", headless: true,
executablePath: process.env.CHROMIUM_BIN executablePath: process.env.CHROMIUM_BIN
}); });
@ -289,6 +289,8 @@ page.on("console", async e => {
page.on("error", e => console.error("[Error]", e.message)); page.on("error", e => console.error("[Error]", e.message));
page.on("pageerror", e => { page.on("pageerror", e => {
if (e.message.includes("Sentry successfully disabled")) return;
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) { if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
console.error("[Page Error]", e.message); console.error("[Page Error]", e.message);
report.otherErrors.push(e.message); report.otherErrors.push(e.message);

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

View file

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

View file

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

View file

@ -16,16 +16,17 @@
* 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 { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Channel, Message } from "discord-types/general"; import { Channel, Message } from "discord-types/general";
import type { MouseEventHandler } from "react"; import type { ComponentType, MouseEventHandler } from "react";
const logger = new Logger("MessagePopover"); const logger = new Logger("MessagePopover");
export interface ButtonItem { export interface ButtonItem {
key?: string, key?: string,
label: string, label: string,
icon: React.ComponentType<any>, icon: ComponentType<any>,
message: Message, message: Message,
channel: Channel, channel: Channel,
onClick?: MouseEventHandler<HTMLButtonElement>, onClick?: MouseEventHandler<HTMLButtonElement>,
@ -48,22 +49,26 @@ export function removeButton(identifier: string) {
} }
export function _buildPopoverElements( export function _buildPopoverElements(
msg: Message, Component: React.ComponentType<ButtonItem>,
makeButton: (item: ButtonItem) => React.ComponentType message: Message
) { ) {
const items = [] as React.ComponentType[]; const items: React.ReactNode[] = [];
for (const [identifier, getItem] of buttons.entries()) { for (const [identifier, getItem] of buttons.entries()) {
try { try {
const item = getItem(msg); const item = getItem(message);
if (item) { if (item) {
item.key ??= identifier; item.key ??= identifier;
items.push(makeButton(item)); items.push(
<ErrorBoundary noop>
<Component {...item} />
</ErrorBoundary>
);
} }
} catch (err) { } catch (err) {
logger.error(`[${identifier}]`, err); logger.error(`[${identifier}]`, err);
} }
} }
return items; return <>{items}</>;
} }

View file

@ -19,6 +19,8 @@
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { openNotificationSettingsModal } from "@components/VencordSettings/NotificationSettings";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
@ -170,24 +172,31 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
</ModalContent> </ModalContent>
<ModalFooter> <ModalFooter>
<Button <Flex>
disabled={log.length === 0} <Button onClick={openNotificationSettingsModal}>
onClick={() => { Notification Settings
Alerts.show({ </Button>
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`, <Button
async onConfirm() { disabled={log.length === 0}
await DataStore.set(KEY, []); color={Button.Colors.RED}
signals.forEach(x => x()); onClick={() => {
}, Alerts.show({
confirmText: "Do it!", title: "Are you sure?",
confirmColor: "vc-notification-log-danger-btn", body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
cancelText: "Nevermind" async onConfirm() {
}); await DataStore.set(KEY, []);
}} signals.forEach(x => x());
> },
Clear Notification Log confirmText: "Do it!",
</Button> confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
</Flex>
</ModalFooter> </ModalFooter>
</ModalRoot> </ModalRoot>
); );

View file

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

View file

@ -1,7 +1,6 @@
.vc-expandableheader-center-flex { .vc-expandableheader-center-flex {
display: flex; display: flex;
justify-items: center; place-items: center;
align-items: center;
} }
.vc-expandableheader-btn { .vc-expandableheader-btn {

28
src/components/Grid.tsx Normal file
View file

@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CSSProperties } from "react";
interface Props {
columns: number;
gap?: string;
inline?: boolean;
}
export function Grid(props: Props & JSX.IntrinsicElements["div"]) {
const style: CSSProperties = {
display: props.inline ? "inline-grid" : "grid",
gridTemplateColumns: `repeat(${props.columns}, 1fr)`,
gap: props.gap,
...props.style
};
return (
<div {...props} style={style}>
{props.children}
</div>
);
}

View file

@ -18,19 +18,17 @@
import "./iconStyles.css"; import "./iconStyles.css";
import { getTheme, Theme } from "@utils/discord";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { i18n } from "@webpack/common"; import { i18n } from "@webpack/common";
import type { PropsWithChildren, SVGProps } from "react"; import type { PropsWithChildren } from "react";
interface BaseIconProps extends IconProps { interface BaseIconProps extends IconProps {
viewBox: string; viewBox: string;
} }
interface IconProps extends SVGProps<SVGSVGElement> { type IconProps = JSX.IntrinsicElements["svg"];
className?: string; type ImageProps = JSX.IntrinsicElements["img"];
height?: string | number;
width?: string | number;
}
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) { function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
return ( return (
@ -67,8 +65,7 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
} }
/** /**
* Discord's copy icon, as seen in the user popout right of the username when clicking * Discord's copy icon, as seen in the user panel popout on the right of the username and in large code blocks
* your own username in the bottom left user panel
*/ */
export function CopyIcon(props: IconProps) { export function CopyIcon(props: IconProps) {
return ( return (
@ -78,8 +75,9 @@ export function CopyIcon(props: IconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<g fill="currentColor"> <g fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" /> <path d="M3 16a1 1 0 0 1-1-1v-5a8 8 0 0 1 8-8h5a1 1 0 0 1 1 1v.5a.5.5 0 0 1-.5.5H10a6 6 0 0 0-6 6v5.5a.5.5 0 0 1-.5.5H3Z" />
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" /> <path d="M6 18a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4h-3a5 5 0 0 1-5-5V6h-4a4 4 0 0 0-4 4v8Z" />
<path d="M21.73 12a3 3 0 0 0-.6-.88l-4.25-4.24a3 3 0 0 0-.88-.61V9a3 3 0 0 0 3 3h2.73Z" />
</g> </g>
</Icon> </Icon>
); );
@ -329,3 +327,103 @@ export function NotesIcon(props: IconProps) {
</Icon> </Icon>
); );
} }
export function FolderIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-folder-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 5a3 3 0 0 1 3-3h3.93a2 2 0 0 1 1.66.9L12 5h7a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Z"
/>
</Icon>
);
}
export function LogIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-log-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M3.11 8H6v10.82c0 .86.37 1.68 1 2.27.46.43 1.02.71 1.63.84A1 1 0 0 0 9 22h10a4 4 0 0 0 4-4v-1a2 2 0 0 0-2-2h-1V5a3 3 0 0 0-3-3H4.67c-.87 0-1.7.32-2.34.9-.63.6-1 1.42-1 2.28 0 .71.3 1.35.52 1.75a5.35 5.35 0 0 0 .48.7l.01.01h.01L3.11 7l-.76.65a1 1 0 0 0 .76.35Zm1.56-4c-.38 0-.72.14-.97.37-.24.23-.37.52-.37.81a1.69 1.69 0 0 0 .3.82H6v-.83c0-.29-.13-.58-.37-.8C5.4 4.14 5.04 4 4.67 4Zm5 13a3.58 3.58 0 0 1 0 3H19a2 2 0 0 0 2-2v-1H9.66ZM3.86 6.35ZM11 8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm-1 5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1Z"
/>
</Icon>
);
}
export function RestartIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-restart-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z"
/>
</Icon>
);
}
export function PaintbrushIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-paintbrush-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M15.35 7.24C15.9 6.67 16 5.8 16 5a3 3 0 1 1 3 3c-.8 0-1.67.09-2.24.65a1.5 1.5 0 0 0 0 2.11l1.12 1.12a3 3 0 0 1 0 4.24l-5 5a3 3 0 0 1-4.25 0l-5.76-5.75a3 3 0 0 1 0-4.24l4.04-4.04.97-.97a3 3 0 0 1 4.24 0l1.12 1.12c.58.58 1.52.58 2.1 0ZM6.9 9.9 4.3 12.54a1 1 0 0 0 0 1.42l2.17 2.17.83-.84a1 1 0 0 1 1.42 1.42l-.84.83.59.59 1.83-1.84a1 1 0 0 1 1.42 1.42l-1.84 1.83.17.17a1 1 0 0 0 1.42 0l2.63-2.62L6.9 9.9Z"
/>
</Icon>
);
}
export function PencilIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-pencil-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z"
/>
</Icon>
);
}
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
export function GithubIcon(props: ImageProps) {
const src = getTheme() === Theme.Light
? GithubIconLight
: GithubIconDark;
return <img {...props} src={src} />;
}
export function WebsiteIcon(props: ImageProps) {
const src = getTheme() === Theme.Light
? WebsiteIconLight
: WebsiteIconDark;
return <img {...props} src={src} />;
}

View file

@ -6,22 +6,16 @@
import "./LinkIconButton.css"; import "./LinkIconButton.css";
import { getTheme, Theme } from "@utils/discord";
import { MaskedLink, Tooltip } from "@webpack/common"; import { MaskedLink, Tooltip } from "@webpack/common";
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg"; import { GithubIcon, WebsiteIcon } from "..";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
export function GithubIcon() { export function GithubLinkIcon() {
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark; return <GithubIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
return <img src={src} aria-hidden className={"vc-settings-modal-link-icon"} />;
} }
export function WebsiteIcon() { export function WebsiteLinkIcon() {
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark; return <WebsiteIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
return <img src={src} aria-hidden className={"vc-settings-modal-link-icon"} />;
} }
interface Props { interface Props {
@ -41,5 +35,5 @@ function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; })
); );
} }
export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteIcon} />; export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteLinkIcon} />;
export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubIcon} />; export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubLinkIcon} />;

View file

@ -27,7 +27,7 @@ import { gitRemote } from "@shared/vencordUserAgent";
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { OptionType, Plugin } from "@utils/types"; import { OptionType, Plugin } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common"; import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
@ -310,3 +310,13 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</ModalRoot> </ModalRoot>
); );
} }
export function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string) => void) {
openModal(modalProps => (
<PluginModal
{...modalProps}
plugin={plugin}
onRestartNeeded={() => onRestartNeeded?.(plugin.name)}
/>
));
}

View file

@ -16,6 +16,8 @@
* 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 { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { OptionType, PluginOptionNumber } from "@utils/types"; import { OptionType, PluginOptionNumber } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common"; import { Forms, React, TextInput } from "@webpack/common";
@ -54,7 +56,8 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle> <Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<TextInput <TextInput
type="number" type="number"
pattern="-?[0-9]+" pattern="-?[0-9]+"

View file

@ -16,6 +16,8 @@
* 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 { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSelect } from "@utils/types"; import { PluginOptionSelect } from "@utils/types";
import { Forms, React, Select } from "@webpack/common"; import { Forms, React, Select } from "@webpack/common";
@ -44,7 +46,8 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle> <Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16} type="description">{option.description}</Forms.FormText>
<Select <Select
isDisabled={option.disabled?.call(definedSettings) ?? false} isDisabled={option.disabled?.call(definedSettings) ?? false}
options={option.options} options={option.options}

View file

@ -16,6 +16,8 @@
* 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 { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSlider } from "@utils/types"; import { PluginOptionSlider } from "@utils/types";
import { Forms, React, Slider } from "@webpack/common"; import { Forms, React, Slider } from "@webpack/common";
@ -50,7 +52,8 @@ export function SettingSliderComponent({ option, pluginSettings, definedSettings
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle> <Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<Slider <Slider
disabled={option.disabled?.call(definedSettings) ?? false} disabled={option.disabled?.call(definedSettings) ?? false}
markers={option.markers} markers={option.markers}

View file

@ -16,6 +16,8 @@
* 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 { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionString } from "@utils/types"; import { PluginOptionString } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common"; import { Forms, React, TextInput } from "@webpack/common";
@ -41,7 +43,8 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle> <Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<TextInput <TextInput
type="text" type="text"
value={state} value={state}

View file

@ -23,7 +23,7 @@ import { showNotice } from "@api/Notices";
import { Settings, useSettings } from "@api/Settings"; import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { CogWheel, InfoIcon } from "@components/Icons"; import { CogWheel, InfoIcon } from "@components/Icons";
import PluginModal from "@components/PluginSettings/PluginModal"; import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { AddonCard } from "@components/VencordSettings/AddonCard"; import { AddonCard } from "@components/VencordSettings/AddonCard";
import { SettingsTab } from "@components/VencordSettings/shared"; import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList"; import { ChangeList } from "@utils/ChangeList";
@ -31,7 +31,6 @@ import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
@ -45,7 +44,7 @@ const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() =>
const cl = classNameFactory("vc-plugins-"); const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189"); const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper"); const InputStyles = findByPropsLazy("inputWrapper", "inputDefault", "error");
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled"); const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
@ -96,14 +95,6 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
const isEnabled = () => settings.enabled ?? false; const isEnabled = () => settings.enabled ?? false;
function openModal() {
openModalLazy(async () => {
return modalProps => {
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
};
});
}
function toggleEnabled() { function toggleEnabled() {
const wasEnabled = isEnabled(); const wasEnabled = isEnabled();
@ -160,7 +151,11 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
infoButton={ infoButton={
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}> <button
role="switch"
onClick={() => openPluginModal(plugin, onRestartNeeded)}
className={classes(ButtonClasses.button, cl("info-button"))}
>
{plugin.options && !isObjectEmpty(plugin.options) {plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel /> ? <CogWheel />
: <InfoIcon />} : <InfoIcon />}
@ -297,10 +292,10 @@ export default function PluginSettings() {
if (!pluginFilter(p)) continue; if (!pluginFilter(p)) continue;
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled); const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);
if (isRequired) { if (isRequired) {
const tooltipText = p.required const tooltipText = p.required || !depMap[p.name]
? "This plugin is required for Vencord to function." ? "This plugin is required for Vencord to function."
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled)); : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
@ -339,8 +334,8 @@ export default function PluginSettings() {
Filters Filters
</Forms.FormTitle> </Forms.FormTitle>
<div className={cl("filter-controls")}> <div className={classes(Margins.bottom20, cl("filter-controls"))}>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} /> <TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} />
<div className={InputStyles.inputWrapper}> <div className={InputStyles.inputWrapper}>
<Select <Select
options={[ options={[
@ -353,6 +348,7 @@ export default function PluginSettings() {
select={onStatusChange} select={onStatusChange}
isSelected={v => v === searchValue.status} isSelected={v => v === searchValue.status}
closeOnSelect={true} closeOnSelect={true}
className={InputStyles.inputDefault}
/> />
</div> </div>
</div> </div>

View file

@ -19,6 +19,7 @@
import { showNotification } from "@api/Notifications"; import { showNotification } from "@api/Notifications";
import { Settings, useSettings } from "@api/Settings"; import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput"; import { CheckedTextInput } from "@components/CheckedTextInput";
import { Grid } from "@components/Grid";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
@ -85,7 +86,9 @@ function SettingsSyncSection() {
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => putCloudSettings(true)} onClick={() => putCloudSettings(true)}
>Sync to Cloud</Button> >
Sync to Cloud
</Button>
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!"> <Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
{({ onMouseLeave, onMouseEnter }) => ( {({ onMouseLeave, onMouseEnter }) => (
<Button <Button
@ -95,7 +98,9 @@ function SettingsSyncSection() {
color={Button.Colors.RED} color={Button.Colors.RED}
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => getCloudSettings(true, true)} onClick={() => getCloudSettings(true, true)}
>Sync from Cloud</Button> >
Sync from Cloud
</Button>
)} )}
</Tooltip> </Tooltip>
<Button <Button
@ -103,7 +108,9 @@ function SettingsSyncSection() {
color={Button.Colors.RED} color={Button.Colors.RED}
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => deleteCloudSettings()} onClick={() => deleteCloudSettings()}
>Delete Cloud Settings</Button> >
Delete Cloud Settings
</Button>
</div> </div>
</Forms.FormSection> </Forms.FormSection>
); );
@ -124,7 +131,12 @@ function CloudTab() {
<Switch <Switch
key="backend" key="backend"
value={settings.cloud.authenticated} value={settings.cloud.authenticated}
onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }} onChange={v => {
if (v)
authorizeCloud();
else
settings.cloud.authenticated = v;
}}
note="This will request authorization if you have not yet set up cloud integrations." note="This will request authorization if you have not yet set up cloud integrations."
> >
Enable Cloud Integrations Enable Cloud Integrations
@ -136,23 +148,43 @@ function CloudTab() {
<CheckedTextInput <CheckedTextInput
key="backendUrl" key="backendUrl"
value={settings.cloud.url} value={settings.cloud.url}
onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }} onChange={async v => {
settings.cloud.url = v;
settings.cloud.authenticated = false;
deauthorizeCloud();
}}
validate={validateUrl} validate={validateUrl}
/> />
<Button
className={Margins.top8} <Grid columns={2} gap="1em" className={Margins.top8}>
size={Button.Sizes.MEDIUM} <Button
color={Button.Colors.RED} size={Button.Sizes.MEDIUM}
disabled={!settings.cloud.authenticated} disabled={!settings.cloud.authenticated}
onClick={() => Alerts.show({ onClick={async () => {
title: "Are you sure?", await deauthorizeCloud();
body: "Once your data is erased, we cannot recover it. There's no going back!", settings.cloud.authenticated = false;
onConfirm: eraseAllData, await authorizeCloud();
confirmText: "Erase it!", }}
confirmColor: "vc-cloud-erase-data-danger-btn", >
cancelText: "Nevermind" Reauthorise
})} </Button>
>Erase All Data</Button> <Button
size={Button.Sizes.MEDIUM}
color={Button.Colors.RED}
disabled={!settings.cloud.authenticated}
onClick={() => Alerts.show({
title: "Are you sure?",
body: "Once your data is erased, we cannot recover it. There's no going back!",
onConfirm: eraseAllData,
confirmText: "Erase it!",
confirmColor: "vc-cloud-erase-data-danger-btn",
cancelText: "Nevermind"
})}
>
Erase All Data
</Button>
</Grid>
<Forms.FormDivider className={Margins.top16} /> <Forms.FormDivider className={Margins.top16} />
</Forms.FormSection > </Forms.FormSection >
<SettingsSyncSection /> <SettingsSyncSection />

View file

@ -0,0 +1,106 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useSettings } from "@api/Settings";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Forms, Select, Slider, Text } from "@webpack/common";
import { ErrorCard } from "..";
export function NotificationSettings() {
const settings = useSettings().notifications;
return (
<div style={{ padding: "1em 0" }}>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
</ErrorCard>
)}
<Forms.FormText className={Margins.bottom8}>
Some plugins may show you notifications. These come in two styles:
<ul>
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
</ul>
</Forms.FormText>
<Select
placeholder="Notification Style"
options={[
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => settings.useNative = v}
isSelected={v => v === settings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={settings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
select={v => settings.position = v}
isSelected={v => v === settings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={settings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={settings.timeout}
onValueChange={v => settings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={settings.logLimit}
onValueChange={v => settings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
</div>
);
}
export function openNotificationSettingsModal() {
openModal(props => (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Settings</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<NotificationSettings />
</ModalContent>
</ModalRoot>
));
}

View file

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

View file

@ -16,24 +16,25 @@
* 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 { useSettings } from "@api/Settings"; import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons"; import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import PluginModal from "@components/PluginSettings/PluginModal"; import { openPluginModal } from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes"; import type { UserThemeHeader } from "@main/themes";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { showItemInFolder } from "@utils/native"; import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack"; import { findLazy } from "@webpack";
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react"; import type { ComponentType, Ref, SyntheticEvent } from "react";
import Plugins from "~plugins";
import { AddonCard } from "./AddonCard"; import { AddonCard } from "./AddonCard";
import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
type FileInput = ComponentType<{ type FileInput = ComponentType<{
@ -43,9 +44,7 @@ type FileInput = ComponentType<{
filters?: { name?: string; extensions: string[]; }[]; filters?: { name?: string; extensions: string[]; }[];
}>; }>;
const InviteActions = findByPropsLazy("resolveInvite");
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef); const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
const cl = classNameFactory("vc-settings-theme-"); const cl = classNameFactory("vc-settings-theme-");
@ -78,8 +77,16 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle> <Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText> <Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div> <div>
{themeLinks.map(link => ( {themeLinks.map(rawLink => {
<Card style={{ const { label, link } = (() => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return { label: rawLink, link: rawLink };
const [, mode, link] = match;
return { label: `[${mode} mode only] ${link}`, link };
})();
return <Card style={{
padding: ".5em", padding: ".5em",
marginBottom: ".5em", marginBottom: ".5em",
marginTop: ".5em" marginTop: ".5em"
@ -87,11 +94,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle tag="h5" style={{ <Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word" overflowWrap: "break-word"
}}> }}>
{link} {label}
</Forms.FormTitle> </Forms.FormTitle>
<Validator link={link} /> <Validator link={link} />
</Card> </Card>;
))} })}
</div> </div>
</> </>
); );
@ -213,60 +220,52 @@ function ThemesTab() {
</Card> </Card>
<Forms.FormSection title="Local Themes"> <Forms.FormSection title="Local Themes">
<Card className="vc-settings-quick-actions-card"> <QuickActionCard>
<> <>
{IS_WEB ? {IS_WEB ?
( (
<Button <QuickAction
size={Button.Sizes.SMALL} text={
disabled={themeDirPending} <span style={{ position: "relative" }}>
> Upload Theme
Upload Theme <FileInput
<FileInput ref={fileInputRef}
ref={fileInputRef} onChange={onFileUpload}
onChange={onFileUpload} multiple={true}
multiple={true} filters={[{ extensions: ["css"] }]}
filters={[{ extensions: ["css"] }]} />
/> </span>
</Button> }
Icon={PlusIcon}
/>
) : ( ) : (
<Button <QuickAction
onClick={() => showItemInFolder(themeDir!)} text="Open Themes Folder"
size={Button.Sizes.SMALL} action={() => showItemInFolder(themeDir!)}
disabled={themeDirPending} disabled={themeDirPending}
> Icon={FolderIcon}
Open Themes Folder />
</Button>
)} )}
<Button <QuickAction
onClick={refreshLocalThemes} text="Load missing Themes"
size={Button.Sizes.SMALL} action={refreshLocalThemes}
> Icon={RestartIcon}
Load missing Themes />
</Button> <QuickAction
<Button text="Edit QuickCSS"
onClick={() => VencordNative.quickCss.openEditor()} action={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL} Icon={PaintbrushIcon}
> />
Edit QuickCSS
</Button>
{Vencord.Settings.plugins.ClientTheme.enabled && ( {Settings.plugins.ClientTheme.enabled && (
<Button <QuickAction
onClick={() => openModal(modalProps => ( text="Edit ClientTheme"
<PluginModal action={() => openPluginModal(Plugins.ClientTheme)}
{...modalProps} Icon={PencilIcon}
plugin={Vencord.Plugins.plugins.ClientTheme} />
onRestartNeeded={() => { }}
/>
))}
size={Button.Sizes.SMALL}
>
Edit ClientTheme
</Button>
)} )}
</> </>
</Card> </QuickActionCard>
<div className={cl("grid")}> <div className={cl("grid")}>
{userThemes?.map(theme => ( {userThemes?.map(theme => (
@ -305,6 +304,7 @@ function ThemesTab() {
<Card className="vc-settings-card vc-text-selectable"> <Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle> <Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText> <Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
</Card> </Card>
@ -312,7 +312,7 @@ function ThemesTab() {
<TextArea <TextArea
value={themeText} value={themeText}
onChange={setThemeText} onChange={setThemeText}
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")} className={"vc-settings-theme-links"}
placeholder="Theme Links" placeholder="Theme Links"
spellCheck={false} spellCheck={false}
onBlur={onBlur} onBlur={onBlur}

View file

@ -17,16 +17,20 @@
*/ */
import { openNotificationLogModal } from "@api/Notifications/notificationLog"; import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings, useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import { ErrorCard } from "@components/ErrorCard"; import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { gitRemote } from "@shared/vencordUserAgent";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity } from "@utils/misc"; import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native"; import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common"; import { Button, Card, Forms, React, Select, Switch } from "@webpack/common";
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
import { openNotificationSettingsModal } from "./NotificationSettings";
import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
const cl = classNameFactory("vc-settings-"); const cl = classNameFactory("vc-settings-");
@ -38,6 +42,7 @@ type KeysOfType<Object, Type> = {
[K in keyof Object]: Object[K] extends Type ? K : never; [K in keyof Object]: Object[K] extends Type ? K : never;
}[keyof Object]; }[keyof Object];
function VencordSettings() { function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, { const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
fallbackValue: "Loading..." fallbackValue: "Loading..."
@ -78,7 +83,7 @@ function VencordSettings() {
!IS_WEB && { !IS_WEB && {
key: "transparent", key: "transparent",
title: "Enable window transparency.", title: "Enable window transparency.",
note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart" note: "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart"
}, },
!IS_WEB && isWindows && { !IS_WEB && isWindows && {
key: "winCtrlQ", key: "winCtrlQ",
@ -96,45 +101,53 @@ function VencordSettings() {
<SettingsTab title="Vencord Settings"> <SettingsTab title="Vencord Settings">
<DonateCard image={donateImage} /> <DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions"> <Forms.FormSection title="Quick Actions">
<Card className={cl("quick-actions-card")}> <QuickActionCard>
<React.Fragment> <QuickAction
{!IS_WEB && ( Icon={LogIcon}
<Button text="Notification Log"
onClick={relaunch} action={openNotificationLogModal}
size={Button.Sizes.SMALL}> />
Restart Client <QuickAction
</Button> Icon={PaintbrushIcon}
)} text="Edit QuickCSS"
<Button action={() => VencordNative.quickCss.openEditor()}
onClick={() => VencordNative.quickCss.openEditor()} />
size={Button.Sizes.SMALL} {!IS_WEB && (
disabled={settingsDir === "Loading..."}> <QuickAction
Open QuickCSS File Icon={RestartIcon}
</Button> text="Relaunch Discord"
{!IS_WEB && ( action={relaunch}
<Button />
onClick={() => showItemInFolder(settingsDir)} )}
size={Button.Sizes.SMALL} {!IS_WEB && (
disabled={settingsDirPending}> <QuickAction
Open Settings Folder Icon={FolderIcon}
</Button> text="Open Settings Folder"
)} action={() => showItemInFolder(settingsDir)}
<Button />
onClick={() => VencordNative.native.openExternal("https://github.com/Vendicated/Vencord")} )}
size={Button.Sizes.SMALL} <QuickAction
disabled={settingsDirPending}> Icon={GithubIcon}
Open in GitHub text="View Source Code"
</Button> action={() => VencordNative.native.openExternal("https://github.com/" + gitRemote)}
</React.Fragment> />
</Card> </QuickActionCard>
</Forms.FormSection> </Forms.FormSection>
<Forms.FormDivider /> <Forms.FormDivider />
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5"> <Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
<Forms.FormText className={Margins.bottom20}> <Forms.FormText className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
Hint: You can change the position of this settings section in the settings of the "Settings" plugin! Hint: You can change the position of this settings section in the
{" "}<Button
look={Button.Looks.BLANK}
style={{ color: "var(--text-link)", display: "inline-block" }}
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
>
settings of the Settings plugin
</Button>!
</Forms.FormText> </Forms.FormText>
{Switches.map(s => s && ( {Switches.map(s => s && (
<Switch <Switch
key={s.key} key={s.key}
@ -212,94 +225,20 @@ function VencordSettings() {
serialize={identity} /> serialize={identity} />
</>} </>}
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />} <Forms.FormSection className={Margins.top16} title="Vencord Notifications" tag="h5">
<Flex>
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button onClick={openNotificationLogModal}>
View Notification Log
</Button>
</Flex>
</Forms.FormSection>
</SettingsTab> </SettingsTab>
); );
} }
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
return (
<>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
</ErrorCard>
)}
<Forms.FormText className={Margins.bottom8}>
Some plugins may show you notifications. These come in two styles:
<ul>
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
</ul>
</Forms.FormText>
<Select
placeholder="Notification Style"
options={[
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => settings.useNative = v}
isSelected={v => v === settings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={settings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
select={v => settings.position = v}
isSelected={v => v === settings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={settings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={settings.timeout}
onValueChange={v => settings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={settings.logLimit}
onValueChange={v => settings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
<Button
onClick={openNotificationLogModal}
disabled={settings.logLimit === 0}
>
Open Notification Log
</Button>
</>
);
}
interface DonateCardProps { interface DonateCardProps {
image: string; image: string;
} }

View file

@ -0,0 +1,33 @@
.vc-settings-quickActions-card {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, max-content));
gap: 0.5em;
justify-content: center;
padding: 0.5em 0;
margin-bottom: 1em;
}
.vc-settings-quickActions-pill {
all: unset;
background: var(--background-secondary);
color: var(--header-secondary);
display: flex;
align-items: center;
gap: 0.5em;
padding: 8px 12px;
border-radius: 9999px;
}
.vc-settings-quickActions-pill:hover {
background: var(--background-secondary-alt);
}
.vc-settings-quickActions-pill:focus-visible {
outline: 2px solid var(--focus-primary);
outline-offset: 2px;
}
.vc-settings-quickActions-img {
width: 24px;
height: 24px;
}

View file

@ -0,0 +1,39 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./quickActions.css";
import { classNameFactory } from "@api/Styles";
import { Card } from "@webpack/common";
import type { ComponentType, PropsWithChildren, ReactNode } from "react";
const cl = classNameFactory("vc-settings-quickActions-");
export interface QuickActionProps {
Icon: ComponentType<{ className?: string; }>;
text: ReactNode;
action?: () => void;
disabled?: boolean;
}
export function QuickAction(props: QuickActionProps) {
const { Icon, action, text, disabled } = props;
return (
<button className={cl("pill")} onClick={action} disabled={disabled}>
<Icon className={cl("img")} />
{text}
</button>
);
}
export function QuickActionCard(props: PropsWithChildren) {
return (
<Card className={cl("card")}>
{props.children}
</Card>
);
}

View file

@ -10,17 +10,6 @@
margin-bottom: -2px; margin-bottom: -2px;
} }
.vc-settings-quick-actions-card {
padding: 1em;
display: flex;
gap: 1em;
align-items: center;
justify-content: space-evenly;
flex-grow: 1;
flex-flow: row wrap;
margin-bottom: 1em;
}
.vc-settings-donate { .vc-settings-donate {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -44,6 +33,20 @@
padding: 0.5em; padding: 0.5em;
border: 1px solid var(--background-modifier-accent); border: 1px solid var(--background-modifier-accent);
max-height: unset; max-height: unset;
background-color: transparent;
box-sizing: border-box;
font-size: 12px;
line-height: 14px;
resize: none;
width: 100%;
}
.vc-settings-theme-links::placeholder {
color: var(--header-secondary);
}
.vc-settings-theme-links:focus {
background-color: var(--background-tertiary);
} }
.vc-cloud-settings-sync-grid { .vc-cloud-settings-sync-grid {

View file

@ -15,9 +15,9 @@ export async function loadLazyChunks() {
try { try {
LazyChunkLoaderLogger.log("Loading all chunks..."); LazyChunkLoaderLogger.log("Loading all chunks...");
const validChunks = new Set<string>(); const validChunks = new Set<number>();
const invalidChunks = new Set<string>(); const invalidChunks = new Set<number>();
const deferredRequires = new Set<string>(); const deferredRequires = new Set<number>();
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);
@ -29,14 +29,14 @@ export async function loadLazyChunks() {
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: number[], entryPoint: number]>();
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
// the chunk containing the component // the chunk containing the component
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
if (chunkIds.length === 0) { if (chunkIds.length === 0) {
return; return;
@ -61,7 +61,7 @@ export async function loadLazyChunks() {
} }
if (!invalidChunkGroup) { if (!invalidChunkGroup) {
validChunkGroups.add([chunkIds, entryPoint]); validChunkGroups.add([chunkIds, Number(entryPoint)]);
} }
})); }));
@ -131,14 +131,14 @@ export async function loadLazyChunks() {
} }
// 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 number[];
// Matches "id" or id: // Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2]; const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue; if (id == null) continue;
allChunks.push(id); allChunks.push(Number(id));
} }
if (allChunks.length === 0) throw new Error("Failed to get all chunks"); if (allChunks.length === 0) throw new Error("Failed to get all chunks");

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 { get } from "@main/utils/simpleGet";
import { IpcEvents } from "@shared/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
@ -25,7 +26,6 @@ import { join } from "path";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import gitRemote from "~git-remote"; import gitRemote from "~git-remote";
import { get } from "../utils/simpleGet";
import { serializeErrors, VENCORD_FILES } from "./common"; import { serializeErrors, VENCORD_FILES } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`; const API_BASE = `https://api.github.com/repos/${gitRemote}`;

View file

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

1
src/modules.d.ts vendored
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/>.
*/ */
// eslint-disable-next-line spaced-comment
/// <reference types="standalone-electron-types"/> /// <reference types="standalone-electron-types"/>
declare module "~plugins" { declare module "~plugins" {

View file

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

View file

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

View file

@ -26,13 +26,8 @@ export default definePlugin({
patches: [{ patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: { replacement: {
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other)) match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.Messages\.MESSAGE_ACTION_REPLY.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/,
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/, replace: "$&,Vencord.Api.MessagePopover._buildPopoverElements($1,$2)"
replace: (m, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");
return `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`;
}
} }
}], }],
}); });

View file

@ -33,8 +33,8 @@ export default definePlugin({
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&" replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
}, },
{ {
match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/, match: /(?<=function (\i)\(\i\){)return null!=(\i)(?=.*NOTICE_DISMISS:\1)/,
replace: "if($1.id==\"VencordNotice\")return($1=null,Vencord.Api.Notices.nextNotice(),true);$&" replace: "if($2.id==\"VencordNotice\")return($2=null,Vencord.Api.Notices.nextNotice(),true);$&"
} }
] ]
} }

View file

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

View file

@ -18,6 +18,7 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
const settings = definePluginSettings({ const settings = definePluginSettings({
@ -47,7 +48,7 @@ export default definePlugin({
}, },
}, },
{ {
find: ".METRICS,", find: ".METRICS",
replacement: [ replacement: [
{ {
match: /this\._intervalId=/, match: /this\._intervalId=/,
@ -71,9 +72,60 @@ export default definePlugin({
startAt: StartAt.Init, startAt: StartAt.Init,
start() { start() {
// Sentry is initialized in its own WebpackInstance.
// It has everything it needs preloaded, so, it doesn't include any chunk loading functionality.
// Because of that, its WebpackInstance doesnt export wreq.m or wreq.c
// To circuvent this and disable Sentry we are gonna hook when wreq.g of its WebpackInstance is set.
// When that happens we are gonna forcefully throw an error and abort everything.
Object.defineProperty(Function.prototype, "g", {
configurable: true,
set(v: any) {
Object.defineProperty(this, "g", {
value: v,
configurable: true,
enumerable: true,
writable: true
});
// Ensure this is most likely the Sentry WebpackInstance.
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
const { stack } = new Error();
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
return;
}
const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
if (!assetPath) {
return;
}
const srcRequest = new XMLHttpRequest();
srcRequest.open("GET", assetPath, false);
srcRequest.send();
// Final condition to see if this is the Sentry WebpackInstance
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
return;
}
new Logger("NoTrack", "#8caaee").info("Disabling Sentry by erroring its WebpackInstance");
Reflect.deleteProperty(Function.prototype, "g");
Reflect.deleteProperty(window, "DiscordSentry");
throw new Error("Sentry successfully disabled");
}
});
Object.defineProperty(window, "DiscordSentry", { Object.defineProperty(window, "DiscordSentry", {
configurable: true, configurable: true,
set() { set() {
new Logger("NoTrack", "#8caaee").error("Failed to disable Sentry. Falling back to deleting window.DiscordSentry");
Reflect.deleteProperty(Function.prototype, "g");
Reflect.deleteProperty(window, "DiscordSentry"); Reflect.deleteProperty(window, "DiscordSentry");
} }
}); });

View file

@ -64,7 +64,7 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}` replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}, },
{ {
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,30}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/, match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,60}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})` replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
} }
] ]

View file

@ -17,6 +17,7 @@
*/ */
import { addAccessory } from "@api/MessageAccessories"; import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
@ -32,12 +33,12 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater"; import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Toasts, UserStore } from "@webpack/common"; import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import plugins, { PluginMeta } from "~plugins"; import plugins, { PluginMeta } from "~plugins";
import settings from "./settings"; import SettingsPlugin from "./settings";
const VENCORD_GUILD_ID = "1015060230222131221"; const VENCORD_GUILD_ID = "1015060230222131221";
const VENBOT_USER_ID = "1017176847865352332"; const VENBOT_USER_ID = "1017176847865352332";
@ -86,7 +87,7 @@ async function generateDebugInfoMessage() {
const info = { const info = {
Vencord: Vencord:
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` + `v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`, `${SettingsPlugin.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
Client: `${RELEASE_CHANNEL} ~ ${client}`, Client: `${RELEASE_CHANNEL} ~ ${client}`,
Platform: window.navigator.platform Platform: window.navigator.platform
}; };
@ -132,12 +133,18 @@ function generatePluginList() {
const checkForUpdatesOnce = onlyOnce(checkForUpdates); const checkForUpdatesOnce = onlyOnce(checkForUpdates);
const settings = definePluginSettings({}).withPrivateSettings<{
dismissedDevBuildWarning?: boolean;
}>();
export default definePlugin({ export default definePlugin({
name: "SupportHelper", name: "SupportHelper",
required: true, required: true,
description: "Helps us provide support to you", description: "Helps us provide support to you",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"], dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"],
settings,
patches: [{ patches: [{
find: ".BEGINNING_DM.format", find: ".BEGINNING_DM.format",
@ -207,17 +214,22 @@ export default definePlugin({
}); });
} }
const repo = await VencordNative.updater.getRepo(); if (!IS_STANDALONE && !settings.store.dismissedDevBuildWarning) {
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
return Alerts.show({ return Alerts.show({
title: "Hold on!", title: "Hold on!",
body: <div> body: <div>
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText> <Forms.FormText>You are using a custom build of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}> <Forms.FormText className={Margins.top8}>
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or We only provide support for <Link href="https://vencord.dev/download">official builds</Link>.
contact your package maintainer for support instead. Either <Link href="https://vencord.dev/download">switch to an official build</Link> or figure your issue out yourself.
</Forms.FormText> </Forms.FormText>
</div>
<Text variant="text-md/bold" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>
</div>,
confirmText: "Understood",
secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => settings.store.dismissedDevBuildWarning = true
}); });
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,28 +16,34 @@
* 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 { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
source: {
description: "Source to replace ban GIF with (Video or Gif)",
type: OptionType.STRING,
default: "https://i.imgur.com/wp5q52C.mp4",
restartNeeded: true,
}
});
export default definePlugin({ export default definePlugin({
name: "BANger", name: "BANger",
description: "Replaces the GIF in the ban dialogue with a custom one.", description: "Replaces the GIF in the ban dialogue with a custom one.",
authors: [Devs.Xinto, Devs.Glitch], authors: [Devs.Xinto, Devs.Glitch],
settings,
patches: [ patches: [
{ {
find: "BAN_CONFIRM_TITLE.", find: "BAN_CONFIRM_TITLE.",
replacement: { replacement: {
match: /src:\i\("?\d+"?\)/g, match: /src:\i\("?\d+"?\)/g,
replace: "src: Vencord.Settings.plugins.BANger.source" replace: "src:$self.source"
} }
} }
], ],
options: { get source() {
source: { return settings.store.source;
description: "Source to replace ban GIF with (Video or Gif)",
type: OptionType.STRING,
default: "https://i.imgur.com/wp5q52C.mp4",
restartNeeded: true,
}
} }
}); });

View file

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

View file

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

View file

@ -16,19 +16,31 @@
* 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 { Settings } from "@api/Settings"; import { definePluginSettings, 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";
import { findByPropsLazy } from "@webpack";
const UserPopoutSectionCssClasses = findByPropsLazy("section", "lastSection"); const settings = definePluginSettings({
hide: {
type: OptionType.BOOLEAN,
description: "Hide notes",
default: false,
restartNeeded: true
},
noSpellCheck: {
type: OptionType.BOOLEAN,
description: "Disable spellcheck in notes",
disabled: () => Settings.plugins.BetterNotesBox.hide,
default: false
}
});
export default definePlugin({ export default definePlugin({
name: "BetterNotesBox", name: "BetterNotesBox",
description: "Hide notes or disable spellcheck (Configure in settings!!)", description: "Hide notes or disable spellcheck (Configure in settings!!)",
authors: [Devs.Ven], authors: [Devs.Ven],
settings,
patches: [ patches: [
{ {
@ -36,7 +48,7 @@ export default definePlugin({
all: true, all: true,
// 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,
predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide, predicate: () => settings.store.hide,
replacement: { replacement: {
match: /hideNote:.+?(?=([,}].*?\)))/g, match: /hideNote:.+?(?=([,}].*?\)))/g,
replace: (m, rest) => { replace: (m, rest) => {
@ -54,37 +66,12 @@ export default definePlugin({
find: "Messages.NOTE_PLACEHOLDER", find: "Messages.NOTE_PLACEHOLDER",
replacement: { replacement: {
match: /\.NOTE_PLACEHOLDER,/, match: /\.NOTE_PLACEHOLDER,/,
replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck," replace: "$&spellCheck:!$self.noSpellCheck,"
}
},
{
find: ".popularApplicationCommandIds,",
replacement: {
match: /lastSection:(!?\i)}\),/,
replace: "$&$self.patchPadding({lastSection:$1}),"
} }
} }
], ],
options: { get noSpellCheck() {
hide: { return settings.store.noSpellCheck;
type: OptionType.BOOLEAN, }
description: "Hide notes",
default: false,
restartNeeded: true
},
noSpellCheck: {
type: OptionType.BOOLEAN,
description: "Disable spellcheck in notes",
disabled: () => Settings.plugins.BetterNotesBox.hide,
default: false
}
},
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
if (!lastSection) return null;
return (
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
);
})
}); });

View file

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

View file

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

View file

@ -25,11 +25,9 @@ export default definePlugin({
description: "Upload with a single click, open menu with right click", description: "Upload with a single click, open menu with right click",
patches: [ patches: [
{ {
find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE", find: '"ChannelAttachButton"',
replacement: { replacement: {
// Discord merges multiple props here with Object.assign() match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
// This patch passes a third object to it with which we override onClick and onContextMenu
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,", replace: "$&onClick:$1,onContextMenu:$2.onClick,",
}, },
}, },

View file

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

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, useStateFromStores } from "@webpack/common"; import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
@ -30,13 +30,12 @@ function onPickColor(color: number) {
updateColorVars(hexColor); updateColorVars(hexColor);
} }
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE",settings:{useSystemTheme:"system"==='); const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
function setTheme(theme: string) { function setTheme(theme: string) {
saveClientTheme({ theme }); saveClientTheme({ theme });
} }
const ThemeStore = findStoreLazy("ThemeStore");
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore"); const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
function ThemeSettings() { function ThemeSettings() {

View file

@ -0,0 +1,5 @@
# ConsoleJanitor
Disables annoying console messages/errors. This plugin mainly removes errors/warnings that happen all the time and Discord logger messages.
One of the disabled messages is the "Window state not initialized" warning, for example.

View file

@ -0,0 +1,144 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, StartAt } from "@utils/types";
const Noop = () => { };
const NoopLogger = {
logDangerously: Noop,
log: Noop,
verboseDangerously: Noop,
verbose: Noop,
info: Noop,
warn: Noop,
error: Noop,
trace: Noop,
time: Noop,
fileOnly: Noop
};
const logAllow = new Set();
const settings = definePluginSettings({
disableLoggers: {
type: OptionType.BOOLEAN,
description: "Disables Discords loggers",
default: false,
restartNeeded: true
},
disableSpotifyLogger: {
type: OptionType.BOOLEAN,
description: "Disable the Spotify logger, which leaks account information and access token",
default: true,
restartNeeded: true
},
whitelistedLoggers: {
type: OptionType.STRING,
description: "Semi colon separated list of loggers to allow even if others are hidden",
default: "GatewaySocket; Routing/Utils",
onChange(newVal: string) {
logAllow.clear();
newVal.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
}
}
});
export default definePlugin({
name: "ConsoleJanitor",
description: "Disables annoying console messages/errors",
authors: [Devs.Nuckyz, Devs.sadan],
settings,
startAt: StartAt.Init,
start() {
logAllow.clear();
this.settings.store.whitelistedLoggers?.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
},
NoopLogger: () => NoopLogger,
shouldLog(logger: string) {
return logAllow.has(logger);
},
patches: [
{
find: 'console.warn("Window state not initialized"',
replacement: {
match: /console\.warn\("Window state not initialized",\i\),/,
replace: ""
}
},
{
find: "is not a valid locale.",
replacement: {
match: /\i\.error\(""\.concat\(\i," is not a valid locale."\)\);/,
replace: ""
}
},
{
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
all: true,
replacement: {
match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/,
replace: ""
}
},
{
find: "RPCServer:WSS",
replacement: {
match: /\i\.error\("Error: "\.concat\((\i)\.message/,
replace: '!$1.message.includes("EADDRINUSE")&&$&'
}
},
{
find: "Tried getting Dispatch instance before instantiated",
replacement: {
match: /null==\i&&\i\.warn\("Tried getting Dispatch instance before instantiated"\),/,
replace: ""
}
},
{
find: "Unable to determine render window for element",
replacement: {
match: /console\.warn\("Unable to determine render window for element",\i\),/,
replace: ""
}
},
{
find: "failed to send analytics events",
replacement: {
match: /console\.error\("\[analytics\] failed to send analytics events query: "\.concat\(\i\)\)/,
replace: ""
}
},
{
find: "Slow dispatch on",
replacement: {
match: /\i\.totalTime>\i&&\i\.verbose\("Slow dispatch on ".+?\)\);/,
replace: ""
}
},
// Patches discords generic logger function
{
find: "Σ:",
predicate: () => settings.store.disableLoggers,
replacement: {
match: /(?<=&&)(?=console)/,
replace: "$self.shouldLog(arguments[0])&&"
}
},
{
find: '("Spotify")',
predicate: () => settings.store.disableSpotifyLogger,
replacement: {
match: /new \i\.\i\("Spotify"\)/,
replace: "$self.NoopLogger()"
}
}
],
});

View file

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

View file

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

View file

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

View file

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

View file

@ -39,12 +39,21 @@ export default definePlugin({
} }
}), }),
patches: [ patches: [
// Only one of the two patches will be at effect; Discord often updates to switch between them.
// See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673
{ {
find: ".ENTER&&(!", find: ".ENTER&&(!",
replacement: { replacement: {
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/, match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
replace: "$self.shouldSubmit($1, $2)" replace: "$self.shouldSubmit($1, $2)"
} }
},
{
find: "!this.hasOpenCodeBlock()",
replacement: {
match: /!(\i).shiftKey&&!(this.hasOpenCodeBlock\(\))&&\(.{0,100}?\)/,
replace: "$self.shouldSubmit($1, $2)"
}
} }
], ],
shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean { shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean {

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes, copyWithToast } from "@utils/misc";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findComponentByCodeLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common"; import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common";
@ -45,7 +45,11 @@ interface Section {
authorIds?: string[]; authorIds?: string[];
} }
function SectionHeader({ section }: { section: Section; }) { interface SectionHeaderProps {
section: Section;
}
function SectionHeader({ section }: SectionHeaderProps) {
const hasSubtitle = typeof section.subtitle !== "undefined"; const hasSubtitle = typeof section.subtitle !== "undefined";
const hasAuthorIds = typeof section.authorIds !== "undefined"; const hasAuthorIds = typeof section.authorIds !== "undefined";
@ -62,6 +66,7 @@ function SectionHeader({ section }: { section: Section; }) {
})(); })();
}, [section.authorIds]); }, [section.authorIds]);
return <div> return <div>
<Flex> <Flex>
<Forms.FormTitle style={{ flexGrow: 1 }}>{section.title}</Forms.FormTitle> <Forms.FormTitle style={{ flexGrow: 1 }}>{section.title}</Forms.FormTitle>
@ -74,8 +79,7 @@ function SectionHeader({ section }: { section: Section; }) {
size={16} size={16}
showUserPopout showUserPopout
className={Margins.bottom8} className={Margins.bottom8}
/> />}
}
</Flex> </Flex>
{hasSubtitle && {hasSubtitle &&
<Forms.FormText type="description" className={Margins.bottom8}> <Forms.FormText type="description" className={Margins.bottom8}>
@ -204,7 +208,16 @@ function ChangeDecorationModal(props: ModalProps) {
{activeSelectedDecoration?.alt} {activeSelectedDecoration?.alt}
</Text> </Text>
} }
{activeDecorationHasAuthor && <Text key={`createdBy-${activeSelectedDecoration.authorId}`}>Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}</Text>} {activeDecorationHasAuthor && (
<Text key={`createdBy-${activeSelectedDecoration.authorId}`}>
Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}
</Text>
)}
{isActiveDecorationPreset && (
<Button onClick={() => copyWithToast(activeDecorationPreset.id)}>
Copy Preset ID
</Button>
)}
</div> </div>
</ErrorBoundary> </ErrorBoundary>
</ModalContent> </ModalContent>

View file

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

View file

@ -24,7 +24,7 @@ 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 { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Emoji } from "@webpack/types"; 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";
@ -203,6 +203,15 @@ export default definePlugin({
settings, settings,
patches: [ patches: [
// Patch the emoji picker in voice calls to not be bypassed by fake nitro
{
find: "emojiItemDisabled]",
predicate: () => settings.store.enableEmojiBypass,
replacement: {
match: /CHAT/,
replace: "STATUS"
}
},
{ {
find: ".PREMIUM_LOCKED;", find: ".PREMIUM_LOCKED;",
group: true, group: true,
@ -245,11 +254,11 @@ export default definePlugin({
}, },
// Allow stickers to be sent everywhere // Allow stickers to be sent everywhere
{ {
find: "canUseCustomStickersEverywhere:function", find: "canUseCustomStickersEverywhere:",
predicate: () => settings.store.enableStickerBypass, predicate: () => settings.store.enableStickerBypass,
replacement: { replacement: {
match: /canUseCustomStickersEverywhere:function\(\i\){/, match: /(?<=canUseCustomStickersEverywhere:)\i/,
replace: "$&return true;" replace: "()=>true"
}, },
}, },
// Make stickers always available // Make stickers always available
@ -263,15 +272,15 @@ export default definePlugin({
}, },
// Allow streaming with high quality // Allow streaming with high quality
{ {
find: "canUseHighVideoUploadQuality:function", find: "canUseHighVideoUploadQuality:",
predicate: () => settings.store.enableStreamQualityBypass, predicate: () => settings.store.enableStreamQualityBypass,
replacement: [ replacement: [
"canUseHighVideoUploadQuality", "canUseHighVideoUploadQuality",
"canStreamQuality", "canStreamQuality",
].map(func => { ].map(func => {
return { return {
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"), match: new RegExp(`(?<=${func}:)\\i`, "g"),
replace: "$&return true;" replace: "()=>true"
}; };
}) })
}, },
@ -286,10 +295,10 @@ export default definePlugin({
}, },
// Allow client themes to be changeable // Allow client themes to be changeable
{ {
find: "canUseClientThemes:function", find: "canUseClientThemes:",
replacement: { replacement: {
match: /canUseClientThemes:function\(\i\){/, match: /(?<=canUseClientThemes:)\i/,
replace: "$&return true;" replace: "()=>true"
} }
}, },
{ {
@ -297,8 +306,8 @@ export default definePlugin({
replacement: [ replacement: [
{ {
// Overwrite incoming connection settings proto with our local settings // Overwrite incoming connection settings proto with our local settings
match: /CONNECTION_OPEN:function\((\i)\){/, match: /function (\i)\((\i)\){(?=.*CONNECTION_OPEN:\1)/,
replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);` replace: (m, funcName, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
}, },
{ {
// Overwrite non local proto changes with our local settings // Overwrite non local proto changes with our local settings
@ -391,10 +400,10 @@ export default definePlugin({
}, },
// Allow using custom app icons // Allow using custom app icons
{ {
find: "canUsePremiumAppIcons:function", find: "canUsePremiumAppIcons:",
replacement: { replacement: {
match: /canUsePremiumAppIcons:function\(\i\){/, match: /(?<=canUsePremiumAppIcons:)\i/,
replace: "$&return true;" replace: "()=>true"
} }
}, },
// Separate patch for allowing using custom app icons // Separate patch for allowing using custom app icons
@ -415,10 +424,10 @@ export default definePlugin({
}, },
// Allow using custom notification sounds // Allow using custom notification sounds
{ {
find: "canUseCustomNotificationSounds:function", find: "canUseCustomNotificationSounds:",
replacement: { replacement: {
match: /canUseCustomNotificationSounds:function\(\i\){/, match: /(?<=canUseCustomNotificationSounds:)\i/,
replace: "$&return true;" replace: "()=>true"
} }
} }
], ],
@ -818,7 +827,14 @@ export default definePlugin({
if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false; if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false;
if (this.canUseEmotes) let isUsableTwitchSubEmote = false;
if (e.managed && e.guildId) {
// @ts-ignore outdated type
const myRoles = GuildMemberStore.getSelfMember(e.guildId)?.roles ?? [];
isUsableTwitchSubEmote = e.roles.some(r => myRoles.includes(r));
}
if (this.canUseEmotes || isUsableTwitchSubEmote)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId); return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
else else
return !e.animated && e.guildId === this.guildId; return !e.animated && e.guildId === this.guildId;

View file

@ -57,7 +57,7 @@ function decode(bio: string): Array<number> | null {
if (bio == null) return null; if (bio == null) return null;
const colorString = bio.match( const colorString = bio.match(
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u, /\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e005d}/u,
); );
if (colorString != null) { if (colorString != null) {
const parsed = [...colorString[0]] const parsed = [...colorString[0]]
@ -121,7 +121,7 @@ export default definePlugin({
{ {
find: "UserProfileStore", find: "UserProfileStore",
replacement: { replacement: {
match: /(?<=getUserProfile\(\i\){return )(\i\[\i\])/, match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/,
replace: "$self.colorDecodeHook($1)" replace: "$self.colorDecodeHook($1)"
} }
}, },

View file

@ -27,7 +27,7 @@ export default definePlugin({
authors: [Devs.D3SOX, Devs.Nickyux], authors: [Devs.D3SOX, Devs.Nickyux],
patches: [ patches: [
{ {
find: ".PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP", find: ".Messages.GUILD_OWNER,",
replacement: { replacement: {
match: /,isOwner:(\i),/, match: /,isOwner:(\i),/,
replace: ",_isOwner:$1=$self.isGuildOwner(e)," replace: ",_isOwner:$1=$self.isGuildOwner(e),"

View file

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

View file

@ -4,95 +4,78 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { classNameFactory } from "@api/Styles";
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 { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Heading, React, RelationshipStore, Text } from "@webpack/common"; import { RelationshipStore, Text } from "@webpack/common";
const container = findByPropsLazy("memberSinceWrapper"); const containerWrapper = findByPropsLazy("memberSinceWrapper");
const container = findByPropsLazy("memberSince");
const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"'); const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"');
const locale = findByPropsLazy("getLocale"); const locale = findByPropsLazy("getLocale");
const lastSection = findByPropsLazy("lastSection"); const Section = findComponentByCodeLazy('"auto":"smooth"', ".section");
const cl = classNameFactory("vc-friendssince-");
export default definePlugin({ export default definePlugin({
name: "FriendsSince", name: "FriendsSince",
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, Devs.Antti],
patches: [ patches: [
// User popup // DM User Sidebar
{ {
find: ".USER_PROFILE}};return", find: ".PANEL}),nicknameIcons",
replacement: { replacement: {
match: /,{userId:(\i.id).{0,30}}\)/, match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id)}\)}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })" replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:true})"
}
},
// User DMs "User Profile" popup in the right
{
find: ".PROFILE_PANEL,",
replacement: {
match: /,{userId:([^,]+?)}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })"
} }
}, },
// User Profile Modal // User Profile Modal
{ {
find: ".userInfoSectionHeader,", find: "action:\"PRESS_APP_CONNECTION\"",
replacement: { replacement: {
match: /(\.Messages\.USER_PROFILE_MEMBER_SINCE.+?userId:(.+?),textClassName:)(\i\.userInfoText)}\)/, match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id),.{0,100}}\)}\),/,
replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSince({ userId: ${userId}, textClassName: ${textClassName} })` replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false}),"
} }
} }
], ],
getFriendSince(userId: string) { FriendsSinceComponent: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {
try {
if (!RelationshipStore.isFriend(userId)) return null;
return RelationshipStore.getSince(userId);
} catch (err) {
new Logger("FriendsSince").error(err);
return null;
}
},
friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
if (!RelationshipStore.isFriend(userId)) return null; if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId); const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null; if (!friendsSince) return null;
return ( return (
<div className={lastSection.section}> <Section heading="Friends Since">
<Heading variant="eyebrow" className={cl("title")}> {
Friends Since isSidebar ? (
</Heading> <Text variant="text-sm/normal">
{getCreatedAtDate(friendsSince, locale.getLocale())}
<div className={container.memberSinceWrapper}> </Text>
{!!getCurrentChannel()?.guild_id && ( ) : (
<svg <div className={containerWrapper.memberSinceWrapper}>
aria-hidden="true" <div className={container.memberSince}>
width="16" {!!getCurrentChannel()?.guild_id && (
height="16" <svg
viewBox="0 0 24 24" aria-hidden="true"
fill="var(--interactive-normal)" width="16"
> height="16"
<path d="M13 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" /> viewBox="0 0 24 24"
<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" /> fill="var(--interactive-normal)"
</svg> >
)} <path d="M13 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" />
<Text variant="text-sm/normal" className={classes(cl("body"), textClassName)}> <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" />
{getCreatedAtDate(friendsSince, locale.getLocale())} </svg>
</Text> )}
</div> <Text variant="text-sm/normal">
</div> {getCreatedAtDate(friendsSince, locale.getLocale())}
</Text>
</div>
</div>
)
}
</Section>
); );
}, { noop: true }) }, { noop: true }),
}); });

View file

@ -1,12 +0,0 @@
/* copy pasted from discord */
.vc-friendssince-title {
display: flex;
font-weight: 700;
margin-bottom: 6px
}
.vc-friendssince-body {
font-size: 14px;
line-height: 18px
}

View file

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

View file

@ -0,0 +1,111 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { NoopComponent } from "@utils/react";
import definePlugin from "@utils/types";
import { filters, findByPropsLazy, waitFor } from "@webpack";
import { ChannelStore, ContextMenuApi, i18n, UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
const { useMessageMenu } = findByPropsLazy("useMessageMenu");
interface CopyIdMenuItemProps {
id: string;
label: string;
}
let CopyIdMenuItem: (props: CopyIdMenuItemProps) => React.ReactElement | null = NoopComponent;
waitFor(filters.componentByCode('"devmode-copy-id-".concat'), m => CopyIdMenuItem = m);
function MessageMenu({ message, channel, onHeightUpdate }) {
const canReport = message.author &&
!(message.author.id === UserStore.getCurrentUser().id || message.author.system);
return useMessageMenu({
navId: "message-actions",
ariaLabel: i18n.Messages.MESSAGE_UTILITIES_A11Y_LABEL,
message,
channel,
canReport,
onHeightUpdate,
onClose: () => ContextMenuApi.closeContextMenu(),
textSelection: "",
favoriteableType: null,
favoriteableId: null,
favoriteableName: null,
itemHref: void 0,
itemSrc: void 0,
itemSafeSrc: void 0,
itemTextContent: void 0,
isFullSearchContextMenu: true
});
}
interface MessageActionsProps {
message: Message;
isFullSearchContextMenu?: boolean;
}
const contextMenuPatch: NavContextMenuPatchCallback = (children, props: MessageActionsProps) => {
if (props?.isFullSearchContextMenu == null) return;
const group = findGroupChildrenByChildId("devmode-copy-id", children, true);
group?.push(
CopyIdMenuItem({ id: props.message.author.id, label: i18n.Messages.COPY_ID_AUTHOR })
);
};
migratePluginSettings("FullSearchContext", "SearchReply");
export default definePlugin({
name: "FullSearchContext",
description: "Makes the message context menu in message search results have all options you'd expect",
authors: [Devs.Ven, Devs.Aria],
patches: [{
find: "onClick:this.handleMessageClick,",
replacement: {
match: /this(?=\.handleContextMenu\(\i,\i\))/,
replace: "$self"
}
}],
handleContextMenu(event: React.MouseEvent, message: Message) {
const channel = ChannelStore.getChannel(message.channel_id);
if (!channel) return;
event.stopPropagation();
ContextMenuApi.openContextMenu(event, contextMenuProps =>
<MessageMenu
message={message}
channel={channel}
onHeightUpdate={contextMenuProps.onHeightUpdate}
/>
);
},
contextMenus: {
"message-actions": contextMenuPatch
}
});

View file

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

View file

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

View file

@ -171,7 +171,7 @@ export default definePlugin({
find: ".handleImageLoad)", find: ".handleImageLoad)",
replacement: [ replacement: [
{ {
match: /placeholderVersion:\i,/, match: /placeholderVersion:\i,(?=.{0,50}children:)/,
replace: "...$self.makeProps(this),$&" replace: "...$self.makeProps(this),$&"
}, },

View file

@ -59,6 +59,8 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
delete patch.group; delete patch.group;
} }
if (patch.predicate && !patch.predicate()) return;
canonicalizeFind(patch); canonicalizeFind(patch);
if (!Array.isArray(patch.replacement)) { if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement]; patch.replacement = [patch.replacement];
@ -70,6 +72,8 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
}); });
} }
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
patches.push(patch); patches.push(patch);
} }
@ -101,6 +105,11 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) {
settings[d].enabled = true; settings[d].enabled = true;
dep.isDependency = true; dep.isDependency = true;
}); });
if (p.commands?.length) {
Plugins.CommandsAPI.isDependency = true;
settings.CommandsAPI.enabled = true;
}
} }
for (const p of pluginsValues) { for (const p of pluginsValues) {

View file

@ -133,10 +133,12 @@ export default definePlugin({
message: message, message: message,
channel: ChannelStore.getChannel(message.channel_id), channel: ChannelStore.getChannel(message.channel_id),
onClick: async () => { onClick: async () => {
await iteratePasswords(message).then((res: string | false) => { const res = await iteratePasswords(message);
if (res) return void this.buildEmbed(message, res);
return void buildDecModal({ message }); if (res)
}); this.buildEmbed(message, res);
else
buildDecModal({ message });
} }
} }
: null; : null;
@ -169,9 +171,9 @@ export default definePlugin({
message.embeds.push({ message.embeds.push({
type: "rich", type: "rich",
title: "Decrypted Message", rawTitle: "Decrypted Message",
color: "0x45f5f5", color: "0x45f5f5",
description: revealed, rawDescription: revealed,
footer: { footer: {
text: "Made with ❤️ by c0dine and Sammy!", text: "Made with ❤️ by c0dine and Sammy!",
}, },

View file

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

View file

@ -1,5 +0,0 @@
# MaskedLinkPaste
Pasting a link while you have text selected will paste your link as a masked link at that location
![](https://github.com/Vendicated/Vencord/assets/78964224/1d3be2c6-7957-44c9-92ec-551069d46c02)

View file

@ -1,38 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants.js";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
const linkRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
const SlateTransforms = findByPropsLazy("insertText", "selectCommandOption");
export default definePlugin({
name: "MaskedLinkPaste",
authors: [Devs.TheSun],
description: "Pasting a link while having text selected will paste a hyperlink",
patches: [
{
find: ".selection,preventEmojiSurrogates:",
replacement: {
match: /(?<=\i.delete.{0,50})(\i)\.insertText\((\i)\)/,
replace: "$self.handlePaste($1, $2, () => $&)"
}
}
],
handlePaste(editor, content: string, originalBehavior: () => void) {
if (content && linkRegex.test(content) && editor.operations?.[0]?.type === "remove_text") {
SlateTransforms.insertText(
editor,
`[${editor.operations[0].text}](${content})`
);
}
else originalBehavior();
}
});

View file

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

View file

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

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