Compare commits

..

36 commits

Author SHA1 Message Date
lewisakura a1ef2c4010
Merge branch 'dev' into feat/translation 2024-07-05 21:07:34 +01:00
lewisakura 946738e069
chore: cloud settings i18n 2024-07-01 13:16:36 +01:00
lewisakura 77d059dd8f
chore: fix i18n ally 2024-07-01 13:13:16 +01:00
lewisakura d216bdb6bb
chore: add reauthorise key 2024-07-01 13:09:21 +01:00
lewisakura 3f3d5379cf
Merge branch 'dev' into feat/translation 2024-07-01 13:08:29 +01:00
Lewis Crichton 70cc39b5c6
chore: remove unnecessary assertions 2024-06-24 19:01:57 +01:00
Lewis Crichton 2cd94221cd
chore: even better if condition 2024-06-24 19:00:54 +01:00
Lewis Crichton faea1c8224
chore: remove unnecessary if 2024-06-24 19:00:26 +01:00
Lewis Crichton e55c6f99e2
fix: objects counted as plural incorrectly 2024-06-23 21:18:43 +01:00
Lewis Crichton cc7675eccb
chore: boop 2024-06-23 21:14:58 +01:00
Lewis Crichton fcc86370f9
Merge branch 'feat/translation' of ssh://github.com/Vendicated/Vencord into feat/translation 2024-06-23 21:11:46 +01:00
Lewis Crichton cc22c15bcb
merge: dev 2024-06-23 21:11:31 +01:00
lewisakura bbd0729ed6
Merge branch 'dev' into feat/translation 2024-06-19 08:43:47 +01:00
Lewis Crichton 51770e96f2
feat: translate clienttheme plugin as test 2024-06-11 23:12:13 +01:00
Lewis Crichton b92a21ac7d
chore: BIKESHEDDING IS OVER, T PREVAILS 2024-06-11 22:31:06 +01:00
Lewis Crichton 8c4aed699d
chore: better path getter func 2024-06-11 22:25:09 +01:00
Lewis Crichton 26c21c2de8
ci: parse $t calls as valid descriptions 2024-06-11 22:21:19 +01:00
Lewis Crichton 15394e106a
chore: fix showConnections?? 2024-06-11 22:03:21 +01:00
lewisakura 9cc9c57f11
Merge branch 'dev' into feat/translation 2024-06-11 22:01:41 +01:00
Lewis Crichton 38624a8661
chore: use Object.create for better semantics 2024-06-09 21:57:54 +01:00
Lewis Crichton ec5f9f78d3
feat: testing top levels 2024-06-09 21:50:18 +01:00
Lewis Crichton ef028edc0d
feat: top level hax 2024-06-09 21:47:03 +01:00
Lewis Crichton a4d4d981e0
chore: more localisation 2024-06-09 10:59:03 +01:00
Lewis Crichton 22e5396684
chore: restructure locale keys 2024-06-08 19:51:56 +01:00
Lewis Crichton d1242633e5
feat: cached translations, use translation coremod, add terrible german 2024-06-08 19:23:56 +01:00
Lewis Crichton becf4a4c4f
Merge branch 'feat/translation' of ssh://github.com/Vendicated/Vencord into feat/translation 2024-06-08 19:03:53 +01:00
lewisakura c5c0732ffe
Merge branch 'dev' into feat/translation 2024-06-08 19:03:48 +01:00
Lewis Crichton f1e1f9cd44
Merge branch 'dev' of ssh://github.com/Vendicated/Vencord into feat/translation 2024-06-08 19:02:50 +01:00
Lewis Crichton d349689c6a
feat: 0 always uses 'zero' plural rule 2024-06-08 18:21:02 +01:00
Lewis Crichton 16549695d1
feat: initial implementation of translation component 2024-06-08 18:20:12 +01:00
Masterjoona caf1779be3
fix showconnections in new profiles (#2567)
Co-authored-by: Vendicated <vendicated@riseup.net>
2024-06-08 16:33:58 +00:00
Lewis Crichton 7ed73b49e5
feat: start translating 2024-06-03 18:31:56 +01:00
Lewis Crichton 9c9a02f9bf
feat: plurality 2024-06-03 17:39:17 +01:00
Lewis Crichton c0111169b8
feat: translation v2 2024-06-01 11:50:18 +01:00
Lewis Crichton 2dc0c20462
Merge branch 'dev' of ssh://github.com/Vendicated/Vencord into dev 2024-06-01 10:37:03 +01:00
Lewis Crichton 42307ccc0e
fix: Vencord_cloudSecret check (#2077)
finally got around to fixing it - `null` is never a valid return value,
it's `undefined` 🤦
2023-12-28 02:02:49 +00:00
178 changed files with 3255 additions and 4118 deletions

98
.eslintrc.json Normal file
View file

@ -0,0 +1,98 @@
{
"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"
}
}

2
.gitignore vendored
View file

@ -18,5 +18,7 @@ lerna-debug.log*
.pnpm-debug.log* .pnpm-debug.log*
*.tsbuildinfo *.tsbuildinfo
src/userplugins
ExtensionCache/ ExtensionCache/
settings/ settings/

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "src/userplugins/vc-message-logger-enhanced"]
path = src/userplugins/vc-message-logger-enhanced
url = https://github.com/Syncxv/vc-message-logger-enhanced.git

View file

@ -1,6 +1,7 @@
{ {
"extends": "stylelint-config-standard", "extends": "stylelint-config-standard",
"rules": { "rules": {
"indentation": 4,
"selector-class-pattern": [ "selector-class-pattern": [
"^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$", "^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$",
{ {

View file

@ -4,6 +4,7 @@
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"GregorBiswanger.json2ts", "GregorBiswanger.json2ts",
"stylelint.vscode-stylelint", "stylelint.vscode-stylelint",
"Vendicated.vencord-companion" "Vendicated.vencord-companion",
"lokalise.i18n-ally"
] ]
} }

16
.vscode/i18n-ally-custom-framework.yml vendored Normal file
View file

@ -0,0 +1,16 @@
languageIds:
- javascript
- typescript
- javascriptreact
- typescriptreact
usageMatchRegex:
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
- "<Translate ?.* i18nKey=\\{?['\"`]({key})['\"`]"
refactorTemplates:
- "t(\"$1\")"
- "{t(\"$1\")}"
- "<Translate i18nKey=\"$1\" />"
monopoly: true

10
.vscode/settings.json vendored
View file

@ -14,10 +14,18 @@
"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",
"type": "Gitea" "type": "Gitea"
} }
] ],
"i18n-ally.namespace": true,
"i18n-ally.localesPaths": ["./translations"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.extract.keygenStyle": "camelCase",
"i18n-ally.sortKeys": true,
"i18n-ally.keystyle": "nested"
} }

View file

@ -1,126 +0,0 @@
/*
* 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.10.1", "version": "1.9.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,13 +21,12 @@
"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", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson", "test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
@ -35,54 +34,54 @@
"testTsc": "tsc --noEmit" "testTsc": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@sapphi-red/web-noise-suppressor": "0.3.5", "@fluent/langneg": "^0.7.0",
"@sapphi-red/web-noise-suppressor": "0.3.3",
"@vap/core": "0.0.12", "@vap/core": "0.0.12",
"@vap/shiki": "0.10.5", "@vap/shiki": "0.10.5",
"fflate": "^0.8.2", "eslint-plugin-simple-header": "^1.0.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": "^5.0.7", "nanoid": "^4.0.2",
"virtual-merge": "^1.0.1" "virtual-merge": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^2.6.1", "@types/chrome": "^0.0.246",
"@types/chrome": "^0.0.269", "@types/diff": "^5.0.3",
"@types/diff": "^5.2.1", "@types/lodash": "^4.14.194",
"@types/lodash": "^4.17.7", "@types/node": "^18.16.3",
"@types/node": "^22.0.3", "@types/react": "^18.2.0",
"@types/react": "^18.3.3", "@types/react-dom": "^18.2.1",
"@types/react-dom": "^18.3.0", "@types/yazl": "^2.4.2",
"@types/yazl": "^2.4.5", "@typescript-eslint/eslint-plugin": "^5.59.1",
"diff": "^5.2.0", "@typescript-eslint/parser": "^5.59.1",
"diff": "^5.1.0",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
"esbuild": "^0.15.18", "esbuild": "^0.15.18",
"eslint": "^9.8.0", "eslint": "^8.46.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "2.1.0", "eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-simple-header": "^1.1.1", "eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^2.0.0",
"eslint-plugin-unused-imports": "^4.0.1", "highlight.js": "10.6.0",
"highlight.js": "10.7.3",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"moment": "^2.30.1", "moment": "^2.29.4",
"puppeteer-core": "^22.15.0", "puppeteer-core": "^19.11.1",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",
"stylelint": "^16.8.1", "stylelint": "^15.6.0",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^33.0.0",
"ts-patch": "^3.2.1", "ts-patch": "^3.1.2",
"ts-pattern": "^5.3.1", "tsx": "^3.12.7",
"tsx": "^4.16.5", "type-fest": "^3.9.0",
"type-fest": "^4.23.0", "typescript": "^5.4.5",
"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@9.8.0": "patches/eslint@9.8.0.patch", "eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
"eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch" "eslint@8.46.0": "patches/eslint@8.46.0.patch"
}, },
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [ "ignoreMissing": [

View file

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

@ -1,14 +0,0 @@
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, commonRendererPlugins, watch } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, 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"),
...commonRendererPlugins ...commonOpts.plugins
], ],
define: { define: {
...defines, ...defines,
@ -180,7 +180,7 @@ await Promise.all([
sourcemap, sourcemap,
plugins: [ plugins: [
globPlugins("vencordDesktop"), globPlugins("vencordDesktop"),
...commonRendererPlugins ...commonOpts.plugins
], ],
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, commonRendererPlugins } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } 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"),
...commonRendererPlugins ...commonOpts.plugins,
], ],
target: ["esnext"], target: ["esnext"],
define: { define: {
@ -116,12 +116,7 @@ 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,7 +28,6 @@ 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"));
@ -294,16 +293,37 @@ export const stylePlugin = {
}; };
/** /**
* @type {(filter: RegExp, message: string) => import("esbuild").Plugin} * @type {import("esbuild").Plugin}
*/ */
export const banImportPlugin = (filter, message) => ({ export const translationPlugin = {
name: "ban-imports", name: "translation-plugin",
setup: build => { setup: ({ onResolve, onLoad }) => {
build.onResolve({ filter }, () => { const filter = /^~translations$/;
return { errors: [{ text: message }] };
onResolve({ filter }, ({ path }) => ({
namespace: "translations", path
}));
onLoad({ filter, namespace: "translations" }, async () => {
const translations = {};
const locales = await readdir("./translations");
for (const locale of locales) {
const translationBundles = await readdir(`./translations/${locale}`);
for (const bundle of translationBundles) {
const name = bundle.replace(/\.json$/, "");
translations[locale] ??= {};
translations[locale][name] = JSON.parse(await readFile(`./translations/${locale}/${bundle}`, "utf-8"));
}
}
return {
contents: `export default ${JSON.stringify(translations)}`,
};
}); });
} }
}); };
/** /**
* @type {import("esbuild").BuildOptions} * @type {import("esbuild").BuildOptions}
@ -316,24 +336,11 @@ export const commonOpts = {
sourcemap: watch ? "inline" : "", sourcemap: watch ? "inline" : "",
legalComments: "linked", legalComments: "linked",
banner, banner,
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin], plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin, translationPlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"], external: ["~plugins", "~git-hash", "~git-remote", "~translations", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"], inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement", jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment", jsxFragment: "VencordFragment",
// 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

@ -20,7 +20,7 @@ import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
import { access, readFile } from "fs/promises"; import { access, readFile } from "fs/promises";
import { join, sep } from "path"; import { join, sep } from "path";
import { normalize as posixNormalize, sep as posixSep } from "path/posix"; import { normalize as posixNormalize, sep as posixSep } from "path/posix";
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript"; import { BigIntLiteral, CallExpression, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, LiteralExpression, NamedDeclaration, Node, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
import { getPluginTarget } from "./utils.mjs"; import { getPluginTarget } from "./utils.mjs";
@ -90,6 +90,38 @@ function parseDevs() {
throw new Error("Could not find Devs constant"); throw new Error("Could not find Devs constant");
} }
function isTranslationExpression(node: Node): node is CallExpression {
if (!isCallExpression(node)) return false;
const literal = node.expression as LiteralExpression;
if (literal.text !== "t") return false;
return true;
}
async function getTranslation(node: Node): Promise<string | null> {
if (!isTranslationExpression(node)) return null;
const translationString = node.arguments[0];
if (!isStringLiteral(translationString)) return null;
const splitPath = translationString.text.split(".");
const namespace = splitPath.shift();
const path = splitPath.join(".");
// load the namespace
const bundle = JSON.parse(
await readFile(`./translations/en/${namespace}.json`, "utf-8")
);
const dotProp = (key: string, object: any) =>
key.split(".").reduce((obj, key) => obj?.[key], object);
return dotProp(path, bundle);
}
async function parseFile(fileName: string) { async function parseFile(fileName: string) {
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest); const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
@ -120,10 +152,16 @@ async function parseFile(fileName: string) {
switch (key) { switch (key) {
case "name": case "name":
case "description":
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`); if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
data[key] = value.text; data[key] = value.text;
break; break;
case "description":
if (isStringLiteral(value))
data[key] = value.text;
else if (isTranslationExpression(value))
data[key] = (await getTranslation(value))!;
else throw fail(`${key} is not a string literal or a translation function call`);
break;
case "patches": case "patches":
data.hasPatches = true; data.hasPatches = true;
break; break;

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: true, headless: "new",
executablePath: process.env.CHROMIUM_BIN executablePath: process.env.CHROMIUM_BIN
}); });

View file

@ -38,6 +38,7 @@ import { patches, PMLogger, startAllPlugins } from "./plugins";
import { localStorage } from "./utils/localStorage"; import { localStorage } from "./utils/localStorage";
import { relaunch } from "./utils/native"; import { relaunch } from "./utils/native";
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync"; import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
import { t } from "./utils/translation";
import { checkForUpdates, update, UpdateLogger } from "./utils/updater"; import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack"; import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common"; import { SettingsRouter } from "./webpack/common";
@ -54,9 +55,8 @@ async function syncSettings() {
) { ) {
// show a notification letting them know and tell them how to fix it // show a notification letting them know and tell them how to fix it
showNotification({ showNotification({
title: "Cloud Integrations", title: t("vencord.cloudIntegrations"),
body: "We've noticed you have cloud integrations enabled in another client! Due to limitations, you will " + body: t("vencord.cloud.integrations.reauthenticate"),
"need to re-authenticate to continue using them. Click here to go to the settings page to do so!",
color: "var(--yellow-360)", color: "var(--yellow-360)",
onClick: () => SettingsRouter.open("VencordCloud") onClick: () => SettingsRouter.open("VencordCloud")
}); });
@ -76,8 +76,8 @@ async function syncSettings() {
// there was an error to notify the user, but besides that we only want to show one notification instead of all // there was an error to notify the user, but besides that we only want to show one notification instead of all
// of the possible ones it has (such as when your settings are newer). // of the possible ones it has (such as when your settings are newer).
showNotification({ showNotification({
title: "Cloud Settings", title: t("vencord.cloudSettings"),
body: "Your settings have been updated! Click here to restart to fully apply changes!", body: t("vencord.cloud.settings.updated"),
color: "var(--green-360)", color: "var(--green-360)",
onClick: relaunch onClick: relaunch
}); });
@ -100,8 +100,8 @@ async function init() {
await update(); await update();
if (Settings.autoUpdateNotification) if (Settings.autoUpdateNotification)
setTimeout(() => showNotification({ setTimeout(() => showNotification({
title: "Vencord has been updated!", title: t("vencord.update.updated"),
body: "Click here to restart", body: t("vencord.update.clickToRestart"),
permanent: true, permanent: true,
noPersist: true, noPersist: true,
onClick: relaunch onClick: relaunch
@ -110,8 +110,8 @@ async function init() {
} }
setTimeout(() => showNotification({ setTimeout(() => showNotification({
title: "A Vencord update is available!", title: t("vencord.update.available"),
body: "Click here to view the update", body: t("vencord.update.clickToView"),
permanent: true, permanent: true,
noPersist: true, noPersist: true,
onClick: openUpdaterModal! onClick: openUpdaterModal!

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Logger } from "@utils/Logger";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import { sendBotMessage } from "./commandHelpers"; import { sendBotMessage } from "./commandHelpers";
@ -47,10 +46,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.untranslatedName || c.displayName) === "shrug")!.options![0]; OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0]; RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
} catch (e) { } catch (e) {
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds); console.error("Failed to load CommandsApi");
} }
return cmds; return cmds;
} as never; } as never;
@ -139,8 +138,6 @@ 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,10 +93,8 @@ 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

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

View file

@ -230,10 +230,6 @@ 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

@ -19,6 +19,7 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/react"; import { LazyComponent } from "@utils/react";
import { t } from "@utils/translation";
import { React } from "@webpack/common"; import { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard"; import { ErrorCard } from "./ErrorCard";
@ -85,11 +86,11 @@ const ErrorBoundary = LazyComponent(() => {
{...this.state} {...this.state}
/>; />;
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console."; const msg = this.props.message || t("vencord.errorBoundaryDescription");
return ( return (
<ErrorCard style={{ overflow: "hidden" }}> <ErrorCard style={{ overflow: "hidden" }}>
<h1>Oh no!</h1> <h1>{t("vencord.ohNo")}</h1>
<p>{msg}</p> <p>{msg}</p>
<code> <code>
{this.state.message} {this.state.message}

View file

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

View file

@ -65,7 +65,8 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
} }
/** /**
* Discord's copy icon, as seen in the user panel popout on the right of the username and in large code blocks * Discord's copy icon, as seen in the user popout right of the username when clicking
* your own username in the bottom left user panel
*/ */
export function CopyIcon(props: IconProps) { export function CopyIcon(props: IconProps) {
return ( return (
@ -75,9 +76,8 @@ export function CopyIcon(props: IconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<g fill="currentColor"> <g fill="currentColor">
<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="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" />
<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="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="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>
); );

View file

@ -12,8 +12,9 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { DevsById } from "@utils/constants"; import { DevsById } from "@utils/constants";
import { fetchUserProfile } from "@utils/discord"; import { fetchUserProfile } from "@utils/discord";
import { classes, pluralise } from "@utils/misc"; import { classes } from "@utils/misc";
import { ModalContent, ModalRoot, openModal } from "@utils/modal"; import { ModalContent, ModalRoot, openModal } from "@utils/modal";
import { t, Translate } from "@utils/translation";
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
@ -60,8 +61,6 @@ function ContributorModal({ user }: { user: User; }) {
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false)); .sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
}, [user.id, user.username]); }, [user.id, user.username]);
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
return ( return (
<> <>
<div className={cl("header")}> <div className={cl("header")}>
@ -88,15 +87,11 @@ function ContributorModal({ user }: { user: User; }) {
</div> </div>
</div> </div>
{plugins.length ? ( <Forms.FormText>
<Forms.FormText> <Translate i18nKey="vencord.pluginContributed" variables={{ count: plugins.length }}>
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}! <Link href="https://vencord.dev/source" />
</Forms.FormText> </Translate>
) : ( </Forms.FormText>
<Forms.FormText>
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
</Forms.FormText>
)}
{!!plugins.length && ( {!!plugins.length && (
<div className={cl("plugins")}> <div className={cl("plugins")}>
@ -105,7 +100,7 @@ function ContributorModal({ user }: { user: User; }) {
key={p.name} key={p.name}
plugin={p} plugin={p}
disabled={p.required ?? false} disabled={p.required ?? false}
onRestartNeeded={() => showToast("Restart to apply changes!")} onRestartNeeded={() => showToast(t("vencord.pluginRestart"))}
/> />
)} )}
</div> </div>

View file

@ -28,6 +28,7 @@ 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, openModal } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { t } from "@utils/translation";
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";
@ -138,7 +139,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
function renderSettings() { function renderSettings() {
if (!hasSettings || !plugin.options) { if (!hasSettings || !plugin.options) {
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>; return <Forms.FormText>{t("vencord.noSettings")}</Forms.FormText>;
} else { } else {
const options = Object.entries(plugin.options).map(([key, setting]) => { const options = Object.entries(plugin.options).map(([key, setting]) => {
if (setting.hidden) return null; if (setting.hidden) return null;
@ -274,7 +275,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</div> </div>
)} )}
<Forms.FormSection className={Margins.bottom16}> <Forms.FormSection className={Margins.bottom16}>
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle> <Forms.FormTitle tag="h3">{t("vencord.settings")}</Forms.FormTitle>
{renderSettings()} {renderSettings()}
</Forms.FormSection> </Forms.FormSection>
</ModalContent> </ModalContent>
@ -289,7 +290,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
> >
Cancel Cancel
</Button> </Button>
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}> <Tooltip text={t("vencord.settingsErrors")} shouldShow={!canSubmit()}>
{({ onMouseEnter, onMouseLeave }) => ( {({ onMouseEnter, onMouseLeave }) => (
<Button <Button
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
@ -299,12 +300,12 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
disabled={!canSubmit()} disabled={!canSubmit()}
> >
Save & Close {t("vencord.saveAndClose")}
</Button> </Button>
)} )}
</Tooltip> </Tooltip>
</Flex> </Flex>
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>} {saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>{t("vencord.settingsSaveError", { saveError })}</Text>}
</Flex> </Flex>
</ModalFooter>} </ModalFooter>}
</ModalRoot> </ModalRoot>

View file

@ -32,6 +32,7 @@ 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 { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { t } from "@utils/translation";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common"; import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common";
@ -64,19 +65,19 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
<Card className={cl("info-card", { "restart-card": required })}> <Card className={cl("info-card", { "restart-card": required })}>
{required ? ( {required ? (
<> <>
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle> <Forms.FormTitle tag="h5">{t("vencord.pluginHeader.reloadHeader")}</Forms.FormTitle>
<Forms.FormText className={cl("dep-text")}> <Forms.FormText className={cl("dep-text")}>
Restart now to apply new plugins and their settings {t("vencord.pluginHeader.reloadDescription")}
</Forms.FormText> </Forms.FormText>
<Button onClick={() => location.reload()}> <Button onClick={() => location.reload()}>
Restart {t("vencord.pluginHeader.restart")}
</Button> </Button>
</> </>
) : ( ) : (
<> <>
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle> <Forms.FormTitle tag="h5">{t("vencord.pluginHeader.managementHeader")}</Forms.FormTitle>
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText> <Forms.FormText>{t("vencord.pluginHeader.iconInformation")}</Forms.FormText>
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText> <Forms.FormText>{t("vencord.pluginHeader.cogWheel")}</Forms.FormText>
</> </>
)} )}
</Card> </Card>
@ -209,10 +210,10 @@ export default function PluginSettings() {
React.useEffect(() => { React.useEffect(() => {
return () => void (changes.hasChanges && Alerts.show({ return () => void (changes.hasChanges && Alerts.show({
title: "Restart required", title: t("vencord.restartRequired"),
body: ( body: (
<> <>
<p>The following plugins require a restart:</p> <p>$t("vencord.pluginsNeedRestart")</p>
<div>{changes.map((s, i) => ( <div>{changes.map((s, i) => (
<> <>
{i > 0 && ", "} {i > 0 && ", "}
@ -221,8 +222,8 @@ export default function PluginSettings() {
))}</div> ))}</div>
</> </>
), ),
confirmText: "Restart now", confirmText: t("vencord.restartNow"),
cancelText: "Later!", cancelText: t("vencord.restartLater"),
onConfirm: () => location.reload() onConfirm: () => location.reload()
})); }));
}, []); }, []);
@ -296,7 +297,7 @@ export default function PluginSettings() {
if (isRequired) { if (isRequired) {
const tooltipText = p.required const tooltipText = p.required
? "This plugin is required for Vencord to function." ? t("vencord.requiredPlugin")
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled)); : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
requiredPlugins.push( requiredPlugins.push(
@ -331,18 +332,18 @@ export default function PluginSettings() {
<ReloadRequiredCard required={changes.hasChanges} /> <ReloadRequiredCard required={changes.hasChanges} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}> <Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
Filters {t("vencord.pluginFilters")}
</Forms.FormTitle> </Forms.FormTitle>
<div className={classes(Margins.bottom20, cl("filter-controls"))}> <div className={classes(Margins.bottom20, cl("filter-controls"))}>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} /> <TextInput autoFocus value={searchValue.value} placeholder={t("vencord.search.placeholder")} onChange={onSearch} />
<div className={InputStyles.inputWrapper}> <div className={InputStyles.inputWrapper}>
<Select <Select
options={[ options={[
{ label: "Show All", value: SearchStatus.ALL, default: true }, { label: t("vencord.search.all"), value: SearchStatus.ALL, default: true },
{ label: "Show Enabled", value: SearchStatus.ENABLED }, { label: t("vencord.search.enabled"), value: SearchStatus.ENABLED },
{ label: "Show Disabled", value: SearchStatus.DISABLED }, { label: t("vencord.search.disabled"), value: SearchStatus.DISABLED },
{ label: "Show New", value: SearchStatus.NEW } { label: t("vencord.search.new"), value: SearchStatus.NEW }
]} ]}
serialize={String} serialize={String}
select={onStatusChange} select={onStatusChange}
@ -353,7 +354,7 @@ export default function PluginSettings() {
</div> </div>
</div> </div>
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle> <Forms.FormTitle className={Margins.top20}>{t("vencord.plugins")}</Forms.FormTitle>
{plugins.length || requiredPlugins.length {plugins.length || requiredPlugins.length
? ( ? (
@ -371,7 +372,7 @@ export default function PluginSettings() {
<Forms.FormDivider className={Margins.top20} /> <Forms.FormDivider className={Margins.top20} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}> <Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
Required Plugins {t("vencord.requiredPlugins")}
</Forms.FormTitle> </Forms.FormTitle>
<div className={cl("grid")}> <div className={cl("grid")}>
{requiredPlugins.length {requiredPlugins.length
@ -386,7 +387,7 @@ export default function PluginSettings() {
function makeDependencyList(deps: string[]) { function makeDependencyList(deps: string[]) {
return ( return (
<React.Fragment> <React.Fragment>
<Forms.FormText>This plugin is required by:</Forms.FormText> <Forms.FormText>{t("vencord.pluginRequiredBy")}</Forms.FormText>
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)} {deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
</React.Fragment> </React.Fragment>
); );

View file

@ -21,6 +21,7 @@ import "./addonCard.css";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Badge } from "@components/Badge"; import { Badge } from "@components/Badge";
import { Switch } from "@components/Switch"; import { Switch } from "@components/Switch";
import { t } from "@utils/translation";
import { Text, useRef } from "@webpack/common"; import { Text, useRef } from "@webpack/common";
import type { MouseEventHandler, ReactNode } from "react"; import type { MouseEventHandler, ReactNode } from "react";
@ -67,7 +68,7 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
> >
{name} {name}
</div> </div>
</div>{isNew && <Badge text="NEW" color="#ED4245" />} </div>{isNew && <Badge text={t("vencord.new")} color="#ED4245" />}
</Text> </Text>
{!!author && ( {!!author && (
<Text variant="text-md/normal" className={cl("author")}> <Text variant="text-md/normal" className={cl("author")}>

View file

@ -20,6 +20,7 @@ import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync"; import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
import { t } from "@utils/translation";
import { Button, Card, Text } from "@webpack/common"; import { Button, Card, Text } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
@ -29,21 +30,19 @@ function BackupRestoreTab() {
<SettingsTab title="Backup & Restore"> <SettingsTab title="Backup & Restore">
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}> <Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
<Flex flexDirection="column"> <Flex flexDirection="column">
<strong>Warning</strong> <strong>{t("vencord.warning")}</strong>
<span>Importing a settings file will overwrite your current settings.</span> <span>{t("vencord.backupAndRestore.importWarning")}</span>
</Flex> </Flex>
</Card> </Card>
<Text variant="text-md/normal" className={Margins.bottom8}> <Text variant="text-md/normal" className={Margins.bottom8}>
You can import and export your Vencord settings as a JSON file. {t("vencord.backupAndRestore.description")}
This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord.
</Text> </Text>
<Text variant="text-md/normal" className={Margins.bottom8}> <Text variant="text-md/normal" className={Margins.bottom8}>
Settings Export contains: {t("vencord.backupAndRestore.exportContains")}
<ul> <ul>
<li>&mdash; Custom QuickCSS</li> <li>&mdash; {t("vencord.backupAndRestore.customQuickcss")}</li>
<li>&mdash; Theme Links</li> <li>&mdash; {t("vencord.backupAndRestore.themeLinks")}</li>
<li>&mdash; Plugin Settings</li> <li>&mdash; {t("vencord.backupAndRestore.pluginSettings")}</li>
</ul> </ul>
</Text> </Text>
<Flex> <Flex>
@ -51,13 +50,13 @@ function BackupRestoreTab() {
onClick={() => uploadSettingsBackup()} onClick={() => uploadSettingsBackup()}
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
> >
Import Settings {t("vencord.backupAndRestore.importSettings")}
</Button> </Button>
<Button <Button
onClick={downloadSettingsBackup} onClick={downloadSettingsBackup}
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
> >
Export Settings {t("vencord.backupAndRestore.exportSettings")}
</Button> </Button>
</Flex> </Flex>
</SettingsTab> </SettingsTab>

View file

@ -24,6 +24,7 @@ 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";
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync"; import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
import { t, Translate } from "@utils/translation";
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common"; import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
@ -46,8 +47,8 @@ async function eraseAllData() {
if (!res.ok) { if (!res.ok) {
cloudLogger.error(`Failed to erase data, API returned ${res.status}`); cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
showNotification({ showNotification({
title: "Cloud Integrations", title: t("vencord.cloudIntegrations"),
body: `Could not erase all data (API returned ${res.status}), please contact support.`, body: t("vencord.cloud.integrations.eraseError", { status: res.status }),
color: "var(--red-360)" color: "var(--red-360)"
}); });
return; return;
@ -57,8 +58,8 @@ async function eraseAllData() {
await deauthorizeCloud(); await deauthorizeCloud();
showNotification({ showNotification({
title: "Cloud Integrations", title: t("vencord.cloudIntegrations"),
body: "Successfully erased all data.", body: t("vencord.cloud.integrations.eraseSuccess"),
color: "var(--green-360)" color: "var(--green-360)"
}); });
} }
@ -70,8 +71,7 @@ function SettingsSyncSection() {
return ( return (
<Forms.FormSection title="Settings Sync" className={Margins.top16}> <Forms.FormSection title="Settings Sync" className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}> <Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with {t("vencord.cloud.settings.description")}
minimal effort.
</Forms.FormText> </Forms.FormText>
<Switch <Switch
key="cloud-sync" key="cloud-sync"
@ -79,7 +79,7 @@ function SettingsSyncSection() {
value={cloud.settingsSync} value={cloud.settingsSync}
onChange={v => { cloud.settingsSync = v; }} onChange={v => { cloud.settingsSync = v; }}
> >
Settings Sync {t("vencord.settingsSync")}
</Switch> </Switch>
<div className="vc-cloud-settings-sync-grid"> <div className="vc-cloud-settings-sync-grid">
<Button <Button
@ -87,9 +87,9 @@ function SettingsSyncSection() {
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => putCloudSettings(true)} onClick={() => putCloudSettings(true)}
> >
Sync to Cloud {t("vencord.cloud.settings.syncToCloud")}
</Button> </Button>
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!"> <Tooltip text={t("vencord.cloud.settings.overwriteWarning")}>
{({ onMouseLeave, onMouseEnter }) => ( {({ onMouseLeave, onMouseEnter }) => (
<Button <Button
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
@ -99,7 +99,7 @@ function SettingsSyncSection() {
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => getCloudSettings(true, true)} onClick={() => getCloudSettings(true, true)}
> >
Sync from Cloud {t("vencord.cloud.settings.syncFromCloud")}
</Button> </Button>
)} )}
</Tooltip> </Tooltip>
@ -109,7 +109,7 @@ function SettingsSyncSection() {
disabled={!sectionEnabled} disabled={!sectionEnabled}
onClick={() => deleteCloudSettings()} onClick={() => deleteCloudSettings()}
> >
Delete Cloud Settings {t("vencord.cloud.settings.deleteCloudSettings")}
</Button> </Button>
</div> </div>
</Forms.FormSection> </Forms.FormSection>
@ -121,12 +121,12 @@ function CloudTab() {
return ( return (
<SettingsTab title="Vencord Cloud"> <SettingsTab title="Vencord Cloud">
<Forms.FormSection title="Cloud Settings" className={Margins.top16}> <Forms.FormSection title={t("vencord.cloudSettings")} className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}> <Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Vencord comes with a cloud integration that adds goodies like settings sync across devices. <Translate i18nKey="vencord.cloud.integrations.description">
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and <Link href="https://vencord.dev/cloud/privacy" />
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you <Link href="https://github.com/Vencord/Backend" />
can host it yourself. </Translate>
</Forms.FormText> </Forms.FormText>
<Switch <Switch
key="backend" key="backend"
@ -137,13 +137,13 @@ function CloudTab() {
else else
settings.cloud.authenticated = v; settings.cloud.authenticated = v;
}} }}
note="This will request authorization if you have not yet set up cloud integrations." note={t("vencord.cloud.integrations.authorizationNote")}
> >
Enable Cloud Integrations {t("vencord.cloud.integrations.enable")}
</Switch> </Switch>
<Forms.FormTitle tag="h5">Backend URL</Forms.FormTitle> <Forms.FormTitle tag="h5">{t("vencord.cloud.integrations.backendUrl")}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8}> <Forms.FormText className={Margins.bottom8}>
Which backend to use when using cloud integrations. {t("vencord.cloud.integrations.backendNote")}
</Forms.FormText> </Forms.FormText>
<CheckedTextInput <CheckedTextInput
key="backendUrl" key="backendUrl"
@ -166,25 +166,24 @@ function CloudTab() {
await authorizeCloud(); await authorizeCloud();
}} }}
> >
Reauthorise {t("vencord.reauthorise")}
</Button> </Button>
<Button <Button
size={Button.Sizes.MEDIUM} size={Button.Sizes.MEDIUM}
color={Button.Colors.RED} color={Button.Colors.RED}
disabled={!settings.cloud.authenticated} disabled={!settings.cloud.authenticated}
onClick={() => Alerts.show({ onClick={() => Alerts.show({
title: "Are you sure?", title: t("vencord.areYouSure"),
body: "Once your data is erased, we cannot recover it. There's no going back!", body: t("vencord.cloud.integrations.eraseWarning"),
onConfirm: eraseAllData, onConfirm: eraseAllData,
confirmText: "Erase it!", confirmText: t("vencord.cloud.integrations.eraseIt"),
confirmColor: "vc-cloud-erase-data-danger-btn", confirmColor: "vc-cloud-erase-data-danger-btn",
cancelText: "Nevermind" cancelText: t("vencord.nevermind")
})} })}
> >
Erase All Data Erase All Data
</Button> </Button>
</Grid> </Grid>
<Forms.FormDivider className={Margins.top16} /> <Forms.FormDivider className={Margins.top16} />
</Forms.FormSection > </Forms.FormSection >
<SettingsSyncSection /> <SettingsSyncSection />

View file

@ -382,7 +382,6 @@ 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,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Settings, useSettings } from "@api/Settings"; import { 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, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons"; import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
@ -25,14 +25,14 @@ 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 { showItemInFolder } from "@utils/native"; import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { findLazy } from "@webpack"; import { t } from "@utils/translation";
import { findByPropsLazy, findLazy } from "@webpack";
import { 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 { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
@ -44,7 +44,9 @@ 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-");
@ -77,16 +79,8 @@ 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(rawLink => { {themeLinks.map(link => (
const { label, link } = (() => { <Card style={{
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"
@ -94,11 +88,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle tag="h5" style={{ <Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word" overflowWrap: "break-word"
}}> }}>
{label} {link}
</Forms.FormTitle> </Forms.FormTitle>
<Validator link={link} /> <Validator link={link} />
</Card>; </Card>
})} ))}
</div> </div>
</> </>
); );
@ -209,60 +203,68 @@ function ThemesTab() {
return ( return (
<> <>
<Card className="vc-settings-card"> <Card className="vc-settings-card">
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle> <Forms.FormTitle tag="h5">{t("vencord.themes.findThemes")}</Forms.FormTitle>
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}> <div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes"> <Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
BetterDiscord Themes {t("vencord.themes.betterDiscord")}
</Link> </Link>
<Link href="https://github.com/search?q=discord+theme">GitHub</Link> <Link href="https://github.com/search?q=discord+theme">GitHub</Link>
</div> </div>
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText> <Forms.FormText>{t("vencord.themes.betterDiscordNote")}</Forms.FormText>
</Card> </Card>
<Forms.FormSection title="Local Themes"> <Forms.FormSection title={t("vencord.themes.local")}>
<QuickActionCard> <Card className="vc-settings-quick-actions-card">
<> <>
{IS_WEB ? {IS_WEB ?
( (
<QuickAction <Button
text={ size={Button.Sizes.SMALL}
<span style={{ position: "relative" }}> disabled={themeDirPending}
Upload Theme >
<FileInput {t("vencord.themes.upload")}
ref={fileInputRef} <FileInput
onChange={onFileUpload} ref={fileInputRef}
multiple={true} onChange={onFileUpload}
filters={[{ extensions: ["css"] }]} multiple={true}
/> filters={[{ extensions: ["css"] }]}
</span> />
} </Button>
Icon={PlusIcon}
/>
) : ( ) : (
<QuickAction <QuickAction
text="Open Themes Folder" text={t("vencord.themes.openFolder")}
action={() => showItemInFolder(themeDir!)} action={() => showItemInFolder(themeDir!)}
disabled={themeDirPending} disabled={themeDirPending}
Icon={FolderIcon} >
/> {t("vencord.themes.openFolder")}
</Button>
)} )}
<QuickAction <Button
text="Load missing Themes" onClick={refreshLocalThemes}
action={refreshLocalThemes} size={Button.Sizes.SMALL}
Icon={RestartIcon} >
/> {t("vencord.themes.loadMissing")}
<QuickAction </Button>
text="Edit QuickCSS" <Button
action={() => VencordNative.quickCss.openEditor()} onClick={() => VencordNative.quickCss.openEditor()}
Icon={PaintbrushIcon} size={Button.Sizes.SMALL}
/> >
{t("vencord.themes.editQuickCss")}
</Button>
{Settings.plugins.ClientTheme.enabled && ( {Vencord.Settings.plugins.ClientTheme.enabled && (
<QuickAction <Button
text="Edit ClientTheme" onClick={() => openModal(modalProps => (
action={() => openPluginModal(Plugins.ClientTheme)} <PluginModal
Icon={PencilIcon} {...modalProps}
/> plugin={Vencord.Plugins.plugins.ClientTheme}
onRestartNeeded={() => { }}
/>
))}
size={Button.Sizes.SMALL}
>
{t("clientTheme.edit")}
</Button>
)} )}
</> </>
</QuickActionCard> </QuickActionCard>
@ -302,17 +304,16 @@ function ThemesTab() {
return ( return (
<> <>
<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">{t("vencord.themes.pasteLinks")}</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>{t("vencord.themes.oneLinkPerLine")}</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText> <Forms.FormText>{t("vencord.themes.useDirect")}</Forms.FormText>
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
</Card> </Card>
<Forms.FormSection title="Online Themes" tag="h5"> <Forms.FormSection title={t("vencord.themes.online")} tag="h5">
<TextArea <TextArea
value={themeText} value={themeText}
onChange={setThemeText} onChange={setThemeText}
className={"vc-settings-theme-links"} className={classes(TextAreaProps.textarea, "vc-settings-theme-links")}
placeholder="Theme Links" placeholder="Theme Links"
spellCheck={false} spellCheck={false}
onBlur={onBlur} onBlur={onBlur}
@ -337,13 +338,13 @@ function ThemesTab() {
className="vc-settings-tab-bar-item" className="vc-settings-tab-bar-item"
id={ThemeTab.LOCAL} id={ThemeTab.LOCAL}
> >
Local Themes {t("vencord.themes.local")}
</TabBar.Item> </TabBar.Item>
<TabBar.Item <TabBar.Item
className="vc-settings-tab-bar-item" className="vc-settings-tab-bar-item"
id={ThemeTab.ONLINE} id={ThemeTab.ONLINE}
> >
Online Themes {t("vencord.themes.online")}
</TabBar.Item> </TabBar.Item>
</TabBar> </TabBar>

View file

@ -33,20 +33,6 @@
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

@ -16,12 +16,11 @@
* 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 { t } from "@utils/translation";
import { maybePromptToUpdate } from "@utils/updater"; import { maybePromptToUpdate } from "@utils/updater";
export function handleComponentFailed() { export function handleComponentFailed() {
maybePromptToUpdate( maybePromptToUpdate(
"Uh Oh! Failed to render this Page." + t("vencord.failureUpdate")
" However, there is an update available that might fix it." +
" Would you like to update and restart now?"
); );
} }

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<number>(); const validChunks = new Set<string>();
const invalidChunks = new Set<number>(); const invalidChunks = new Set<string>();
const deferredRequires = new Set<number>(); const deferredRequires = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void; let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r); const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
@ -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: number[], entryPoint: number]>(); const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
// 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 => Number(m[1])) : []; const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => 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, Number(entryPoint)]); validChunkGroups.add([chunkIds, 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 number[]; const allChunks = [] as string[];
// Matches "id" or id: // Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) { for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2]; const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue; if (id == null) continue;
allChunks.push(Number(id)); allChunks.push(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,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { 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";
@ -26,6 +25,7 @@ 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,8 +35,7 @@ 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");

6
src/modules.d.ts vendored
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/>.
*/ */
// eslint-disable-next-line spaced-comment
/// <reference types="standalone-electron-types"/> /// <reference types="standalone-electron-types"/>
declare module "~plugins" { declare module "~plugins" {
@ -42,6 +43,11 @@ declare module "~git-remote" {
export default remote; export default remote;
} }
declare module "~translations" {
const translations: Record<string, Record<string, any>>;
export default translations;
}
declare module "file://*" { declare module "file://*" {
const content: string; const content: string;
export default content; export default content;

View file

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

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 "./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";
@ -60,8 +62,36 @@ 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: ".FULL_SIZE]:26", find: 'id:"premium",',
replacement: [
{
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
replace: "$&$1.unshift(...$self.getBadges(arguments[0]));",
},
{
// alt: "", aria-hidden: false, src: originalSrc
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/,
// ...badge.props, ..., src: badge.image ?? ...
replace: "...$1.props,$& $1.image??"
},
// replace their component with ours if applicable
{
match: /(?<=text:(\i)\.description,spacing:12,.{0,50})children:/,
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
},
// conditionally override their onClick with badge.onClick if it exists
{
match: /href:(\i)\.link/,
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
}
]
},
/* new profiles */
{
find: ".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));$&"
@ -77,7 +107,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??" replace: "...$1.props,$& $1.image??"
}, },
{ {
match: /(?<=text:(\i)\.description,.{0,200})children:/, match: /(?<=text:(\i)\.description,.{0,50})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,8 +26,13 @@ export default definePlugin({
patches: [{ patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: { replacement: {
match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.Messages\.MESSAGE_ACTION_REPLY.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/, // foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
replace: "$&,Vencord.Api.MessagePopover._buildPopoverElements($1,$2)" match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
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

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

View file

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

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,60}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/, match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,30}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})` replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
} }
] ]

View file

@ -0,0 +1,27 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import { setLocale } from "@utils/translation";
import definePlugin from "@utils/types";
import { i18n } from "@webpack/common";
export default definePlugin({
name: "Translation",
required: true,
description: "Assists with translating Vencord",
authors: [Devs.lewisakura],
flux: {
USER_SETTINGS_PROTO_UPDATE({ settings }) {
setLocale(settings.proto.localization.locale.value);
}
},
start() {
setLocale(i18n.getLocale());
}
});

View file

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

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

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

View file

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

View file

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

View file

@ -16,34 +16,28 @@
* 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:$self.source" replace: "src: Vencord.Settings.plugins.BANger.source"
} }
} }
], ],
get source() { options: {
return settings.store.source; 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

@ -132,8 +132,8 @@ export default definePlugin({
}, },
// Export the isBetterFolders variable to the folders component // Export the isBetterFolders variable to the folders component
{ {
match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/, match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,)/,
replace: '$&isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,' replace: 'isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
} }
] ]
}, },
@ -249,10 +249,6 @@ export default definePlugin({
dispatchingFoldersClose = false; dispatchingFoldersClose = false;
}); });
} }
},
LOGOUT() {
closeFolders();
} }
}, },

View file

@ -16,31 +16,19 @@
* 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, Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
const settings = definePluginSettings({ const UserPopoutSectionCssClasses = findByPropsLazy("section", "lastSection");
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: [
{ {
@ -48,7 +36,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: () => settings.store.hide, predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide,
replacement: { replacement: {
match: /hideNote:.+?(?=([,}].*?\)))/g, match: /hideNote:.+?(?=([,}].*?\)))/g,
replace: (m, rest) => { replace: (m, rest) => {
@ -66,12 +54,37 @@ export default definePlugin({
find: "Messages.NOTE_PLACEHOLDER", find: "Messages.NOTE_PLACEHOLDER",
replacement: { replacement: {
match: /\.NOTE_PLACEHOLDER,/, match: /\.NOTE_PLACEHOLDER,/,
replace: "$&spellCheck:!$self.noSpellCheck," replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck,"
}
},
{
find: ".popularApplicationCommandIds,",
replacement: {
match: /lastSection:(!?\i)}\),/,
replace: "$&$self.patchPadding({lastSection:$1}),"
} }
} }
], ],
get noSpellCheck() { options: {
return settings.store.noSpellCheck; 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
}
},
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
if (!lastSection) return null;
return (
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
);
})
}); });

View file

@ -25,9 +25,11 @@ 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: '"ChannelAttachButton"', find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE",
replacement: { replacement: {
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/, // Discord merges multiple props here with Object.assign()
// 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

@ -10,9 +10,10 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { t } from "@utils/translation";
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, ThemeStore, useStateFromStores } from "@webpack/common"; import { Button, Forms, 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,12 +31,13 @@ function onPickColor(color: number) {
updateColorVars(hexColor); updateColorVars(hexColor);
} }
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"==='); const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE",settings:{useSystemTheme:"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() {
@ -63,8 +65,8 @@ function ThemeSettings() {
<div className="client-theme-settings"> <div className="client-theme-settings">
<div className="client-theme-container"> <div className="client-theme-container">
<div className="client-theme-settings-labels"> <div className="client-theme-settings-labels">
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle> <Forms.FormTitle tag="h3">{t("clientTheme.settingsTitle")}</Forms.FormTitle>
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText> <Forms.FormText>{t("clientTheme.settingsDescription")}</Forms.FormText>
</div> </div>
<ColorPicker <ColorPicker
color={parseInt(settings.store.color, 16)} color={parseInt(settings.store.color, 16)}
@ -77,12 +79,12 @@ function ThemeSettings() {
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} /> <Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}> <div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
<div className="client-theme-warning"> <div className="client-theme-warning">
<Forms.FormText>Warning, your theme won't look good:</Forms.FormText> <Forms.FormText>{t("clientTheme.warningTitle")}</Forms.FormText>
{contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>} {contrastWarning && <Forms.FormText>{t("clientTheme.warnings.badContrast")}</Forms.FormText>}
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>} {nitroThemeEnabled && <Forms.FormText>{t("clientTheme.warnings.nitro")}</Forms.FormText>}
</div> </div>
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>} {(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>{t(`clientTheme.switchToOpposite.${oppositeTheme}`)}</Button>}
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>} {(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>{t("clientTheme.disableNitroTheme")}</Button>}
</div> </div>
</>)} </>)}
</div> </div>
@ -91,18 +93,18 @@ function ThemeSettings() {
const settings = definePluginSettings({ const settings = definePluginSettings({
color: { color: {
description: "Color your Discord client theme will be based around. Light mode isn't supported", description: t("clientTheme.colorDescription"),
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
default: "313338", default: "313338",
component: () => <ThemeSettings /> component: () => <ThemeSettings />
}, },
resetColor: { resetColor: {
description: "Reset Theme Color", description: t("clientTheme.resetColorDescription"),
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
default: "313338", default: "313338",
component: () => ( component: () => (
<Button onClick={() => onPickColor(0x313338)}> <Button onClick={() => onPickColor(0x313338)}>
Reset Theme Color {t("clientTheme.resetButton")}
</Button> </Button>
) )
} }
@ -111,7 +113,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "ClientTheme", name: "ClientTheme",
authors: [Devs.F53, Devs.Nuckyz], authors: [Devs.F53, Devs.Nuckyz],
description: "Recreation of the old client theme experiment. Add a color to your Discord client theme", description: t("clientTheme.description"),
settings, settings,
startAt: StartAt.DOMContentLoaded, startAt: StartAt.DOMContentLoaded,

View file

@ -60,6 +60,13 @@ export default definePlugin({
replace: "" replace: ""
} }
}, },
{
find: "notosans-400-normalitalic",
replacement: {
match: /,"notosans-.+?"/g,
replace: ""
}
},
{ {
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");', find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
all: true, all: true,
@ -107,7 +114,7 @@ export default definePlugin({
'("MessageActionCreators")', '("ChannelMessages")', '("MessageActionCreators")', '("ChannelMessages")',
'("Routing/Utils")', '("RTCControlSocket")', '("Routing/Utils")', '("RTCControlSocket")',
'("ConnectionEventFramerateReducer")', '("RTCLatencyTestManager")', '("ConnectionEventFramerateReducer")', '("RTCLatencyTestManager")',
'("OverlayBridgeStore")', '("RPCServer:WSS")', '("RPCServer:IPC")' '("OverlayBridgeStore")', '("RPCServer:WSS")'
].map(logger => ({ ].map(logger => ({
find: logger, find: logger,
predicate: () => settings.store.disableNoisyLoggers, predicate: () => settings.store.disableNoisyLoggers,

View file

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

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

@ -1,8 +0,0 @@
.vc-cfc-button {
color: var(--interactive-normal);
cursor: pointer;
}
.vc-cfc-button:hover {
color: var(--interactive-hover);
}

View file

@ -39,15 +39,6 @@ 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&&(!",
replacement: {
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
replace: "$self.shouldSubmit($1, $2)"
}
},
{ {
find: "!this.hasOpenCodeBlock()", find: "!this.hasOpenCodeBlock()",
replacement: { replacement: {

View file

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

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, dearrowByDefault } = settings.store; const { replaceElements } = settings.store;
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return; if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
@ -63,22 +63,18 @@ async function embedDidMount(this: Component<Props>) {
if (!hasTitle && !hasThumb) return; if (!hasTitle && !hasThumb) return;
embed.dearrow = { embed.dearrow = {
enabled: dearrowByDefault enabled: true
}; };
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) { if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
const replacementTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2"); embed.dearrow.oldTitle = embed.rawTitle;
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) {
const replacementProxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
embed.dearrow.oldThumb = dearrowByDefault ? embed.thumbnail.proxyURL : replacementProxyURL; if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
if (dearrowByDefault) embed.thumbnail.proxyURL = replacementProxyURL; embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
} }
this.forceUpdate(); this.forceUpdate();
@ -100,7 +96,6 @@ 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;
@ -158,12 +153,6 @@ 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

@ -91,7 +91,7 @@ export default definePlugin({
replacement: [ replacement: [
// Use Decor avatar decoration hook // Use Decor avatar decoration hook
{ {
match: /(?<=\i\)\({avatarDecoration:)(\i)(?=,)(?<=currentUser:(\i).+?)/, match: /(?<=\i\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/,
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, copyWithToast } from "@utils/misc"; import { classes } 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,11 +45,7 @@ interface Section {
authorIds?: string[]; authorIds?: string[];
} }
interface SectionHeaderProps { function SectionHeader({ section }: { section: Section; }) {
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";
@ -66,7 +62,6 @@ function SectionHeader({ section }: SectionHeaderProps) {
})(); })();
}, [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>
@ -79,7 +74,8 @@ function SectionHeader({ section }: SectionHeaderProps) {
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}>
@ -208,16 +204,7 @@ function ChangeDecorationModal(props: ModalProps) {
{activeSelectedDecoration?.alt} {activeSelectedDecoration?.alt}
</Text> </Text>
} }
{activeDecorationHasAuthor && ( {activeDecorationHasAuthor && <Text key={`createdBy-${activeSelectedDecoration.authorId}`}>Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}</Text>}
<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

@ -88,8 +88,8 @@ export default definePlugin({
{ {
find: "useCanFavoriteChannel", find: "useCanFavoriteChannel",
replacement: { replacement: {
match: /\i\.isDM\(\)\|\|\i\.isThread\(\)/, match: /!\(\i\.isDM\(\)\|\|\i\.isThread\(\)\)/,
replace: "false", replace: "true",
} }
} }
], ],

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, GuildMemberStore, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { 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";
@ -818,14 +818,7 @@ export default definePlugin({
if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false; if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false;
let isUsableTwitchSubEmote = false; if (this.canUseEmotes)
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}]{1,6})\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e005d}/u, /\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,
); );
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 )(.+?)(?=})/, match: /(?<=getUserProfile\(\i\){return )(\i\[\i\])/,
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: ".Messages.GUILD_OWNER,", find: ".PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP",
replacement: { replacement: {
match: /,isOwner:(\i),/, match: /,isOwner:(\i),/,
replace: ",_isOwner:$1=$self.isGuildOwner(e)," replace: ",_isOwner:$1=$self.isGuildOwner(e),"

View file

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

View file

@ -1,13 +0,0 @@
# 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,11 +26,6 @@ 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")!;
@ -75,17 +70,14 @@ 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);
recalculateActivities(); // Trigger activities recalculation
}
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 filter list</Forms.FormText> <Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the allowed list</Forms.FormText>
<div> <div>
<Button <Button
onClick={() => { onClick={() => {
@ -94,7 +86,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 = idsListPushID?.(id); const isAlreadyAdded = allowedIdsPushID?.(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);
} }
@ -107,39 +99,39 @@ function ImportCustomRPCComponent() {
); );
} }
let idsListPushID: ((id: string) => boolean) | null = null; let allowedIdsPushID: ((id: string) => boolean) | null = null;
function IdsListComponent(props: { setValue: (value: string) => void; }) { function AllowedIdsComponent(props: { setValue: (value: string) => void; }) {
const [idsList, setIdsList] = useState<string>(settings.store.idsList ?? ""); const [allowedIds, setAllowedIds] = useState<string>(settings.store.allowedIds ?? "");
idsListPushID = (id: string) => { allowedIdsPushID = (id: string) => {
const currentIds = new Set(idsList.split(",").map(id => id.trim()).filter(Boolean)); const currentIds = new Set(allowedIds.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(", ");
setIdsList(ids); setAllowedIds(ids);
props.setValue(ids); props.setValue(ids);
return isAlreadyAdded; return isAlreadyAdded;
}; };
useEffect(() => () => { useEffect(() => () => {
idsListPushID = null; allowedIdsPushID = null;
}, []); }, []);
function handleChange(newValue: string) { function handleChange(newValue: string) {
setIdsList(newValue); setAllowedIds(newValue);
props.setValue(newValue); props.setValue(newValue);
} }
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle tag="h3">Filter List</Forms.FormTitle> <Forms.FormTitle tag="h3">Allowed List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText> <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>
<TextInput <TextInput
type="text" type="text"
value={idsList} value={allowedIds}
onChange={handleChange} onChange={handleChange}
placeholder="235834946571337729, 343383572805058560" placeholder="235834946571337729, 343383572805058560"
/> />
@ -153,62 +145,40 @@ const settings = definePluginSettings({
description: "", description: "",
component: () => <ImportCustomRPCComponent /> component: () => <ImportCustomRPCComponent />
}, },
listMode: { allowedIds: {
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.idsList = Array.from(ids).join(", "); settings.store.allowedIds = Array.from(ids).join(", ");
recalculateActivities();
}, },
component: props => <IdsListComponent setValue={props.setValue} /> component: props => <AllowedIdsComponent 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[];
@ -219,8 +189,8 @@ function getIgnoredActivities() {
} }
function isActivityTypeIgnored(type: number, id?: string) { function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.idsList.includes(id)) { if (id && settings.store.allowedIds.includes(id)) {
return settings.store.listMode === FilterMode.Blacklist; return false;
} }
switch (type) { switch (type) {
@ -236,8 +206,8 @@ function isActivityTypeIgnored(type: number, id?: string) {
export default definePlugin({ export default definePlugin({
name: "IgnoreActivities", name: "IgnoreActivities",
authors: [Devs.Nuckyz, Devs.Kylie], authors: [Devs.Nuckyz],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below", 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,
@ -266,7 +236,6 @@ 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: {
@ -275,21 +244,15 @@ export default definePlugin({
}, },
}, },
{ {
find: ".promotedLabelWrapperNonBanner,children", find: ".activityCardDetails,children",
replacement: { replacement: {
match: /\.appDetailsHeaderContainer.+?children:\i.*?}\),(?<=application:(\i).+?)/, match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
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) {
@ -316,7 +279,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.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id)); return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.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,(?=.{0,50}children:)/, match: /placeholderVersion:\i,/,
replace: "...$self.makeProps(this),$&" replace: "...$self.makeProps(this),$&"
}, },

View file

@ -66,14 +66,14 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
patch.replacement = [patch.replacement]; patch.replacement = [patch.replacement];
} }
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
if (IS_REPORTER) { if (IS_REPORTER) {
patch.replacement.forEach(r => { patch.replacement.forEach(r => {
delete r.predicate; delete r.predicate;
}); });
} }
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
patches.push(patch); patches.push(patch);
} }

View file

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

View file

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

@ -0,0 +1,38 @@
/*
* 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,16 +5,15 @@
*/ */
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, ThreadMemberListStore } from "."; import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat } 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],
@ -31,19 +30,10 @@ 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 (!guildId || onlineMemberMap.has(guildId)) return; if (onlineMemberMap.has(guildId)) return;
preloadQueue.push(() => preloadQueue.push(() =>
this._ensureCount(guildId) this._ensureCount(guildId)

View file

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

View file

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

View file

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

View file

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

View file

@ -147,7 +147,6 @@ async function fetchMessage(channelID: string, messageID: string) {
if (!msg) return; if (!msg) return;
const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id); const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
if (!message) return;
messageCache.set(message.id, { messageCache.set(message.id, {
message, message,

View file

@ -1,91 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findByPropsLazy } from "@webpack";
import { TabBar, Text, Timestamp, TooltipContainer, useState } from "@webpack/common";
import { parseEditContent } from ".";
const CodeContainerClasses = findByPropsLazy("markup", "codeContainer");
const MiscClasses = findByPropsLazy("messageContent", "markupRtl");
const cl = classNameFactory("vc-ml-modal-");
export function openHistoryModal(message: any) {
openModal(props =>
<ErrorBoundary>
<HistoryModal
modalProps={props}
message={message}
/>
</ErrorBoundary>
);
}
export function HistoryModal({ modalProps, message }: { modalProps: ModalProps; message: any; }) {
const [currentTab, setCurrentTab] = useState(message.editHistory.length);
const timestamps = [message.firstEditTimestamp, ...message.editHistory.map(m => m.timestamp)];
const contents = [...message.editHistory.map(m => m.content), message.content];
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalHeader className={cl("head")}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Message Edit History</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent className={cl("contents")}>
<TabBar
type="top"
look="brand"
className={classes("vc-settings-tab-bar", cl("tab-bar"))}
selectedItem={currentTab}
onItemSelect={setCurrentTab}
>
{message.firstEditTimestamp.getTime() !== message.timestamp.getTime() && (
<TooltipContainer text="This edit state was not logged so it can't be displayed.">
<TabBar.Item
className="vc-settings-tab-bar-item"
id={-1}
disabled
>
<Timestamp
className={cl("timestamp")}
timestamp={message.timestamp}
isEdited={true}
isInline={false}
/>
</TabBar.Item>
</TooltipContainer>
)}
{timestamps.map((timestamp, index) => (
<TabBar.Item
className="vc-settings-tab-bar-item"
id={index}
>
<Timestamp
className={cl("timestamp")}
timestamp={timestamp}
isEdited={true}
isInline={false}
/>
</TabBar.Item>
))}
</TabBar>
<div className={classes(CodeContainerClasses.markup, MiscClasses.messageContent, Margins.top20)}>
{parseEditContent(contents[currentTab], message)}
</div>
</ModalContent>
</ModalRoot>
);
}

View file

@ -24,26 +24,21 @@ import { Settings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles"; import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, i18n, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common"; import { ChannelStore, FluxDispatcher, i18n, Menu, MessageStore, Parser, Timestamp, UserStore, useStateFromStores } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
import overlayStyle from "./deleteStyleOverlay.css?managed"; import overlayStyle from "./deleteStyleOverlay.css?managed";
import textStyle from "./deleteStyleText.css?managed"; import textStyle from "./deleteStyleText.css?managed";
import { openHistoryModal } from "./HistoryModal";
interface MLMessage extends Message { interface MLMessage extends Message {
deleted?: boolean; deleted?: boolean;
editHistory?: { timestamp: Date; content: string; }[]; editHistory?: { timestamp: Date; content: string; }[];
firstEditTimestamp?: Date;
} }
const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessage"); const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessage");
const getMessage = findByCodeLazy('replace(/^\\n+|\\n+$/g,"")');
function addDeleteStyle() { function addDeleteStyle() {
if (Settings.plugins.MessageLogger.deleteStyle === "text") { if (Settings.plugins.MessageLogger.deleteStyle === "text") {
@ -130,28 +125,15 @@ const patchChannelContextMenu: NavContextMenuPatchCallback = (children, { channe
); );
}; };
export function parseEditContent(content: string, message: Message) {
return Parser.parse(content, true, {
channelId: message.channel_id,
messageId: message.id,
allowLinks: true,
allowHeading: true,
allowList: true,
allowEmojiLinks: true,
viewingChannelId: SelectedChannelStore.getChannelId(),
});
}
export default definePlugin({ export default definePlugin({
name: "MessageLogger", name: "MessageLogger",
description: "Temporarily logs deleted and edited messages.", description: "Temporarily logs deleted and edited messages.",
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi], authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux],
dependencies: ["MessageUpdaterAPI"], dependencies: ["MessageUpdaterAPI"],
contextMenus: { contextMenus: {
"message": patchMessageContextMenu, "message": patchMessageContextMenu,
"channel-context": patchChannelContextMenu, "channel-context": patchChannelContextMenu,
"thread-context": patchChannelContextMenu,
"user-context": patchChannelContextMenu, "user-context": patchChannelContextMenu,
"gdm-context": patchChannelContextMenu "gdm-context": patchChannelContextMenu
}, },
@ -168,11 +150,11 @@ export default definePlugin({
(oldMsg, newMsg) => oldMsg?.editHistory === newMsg?.editHistory (oldMsg, newMsg) => oldMsg?.editHistory === newMsg?.editHistory
); );
return Settings.plugins.MessageLogger.inlineEdits && ( return (
<> <>
{message.editHistory?.map(edit => ( {message.editHistory?.map(edit => (
<div className="messagelogger-edited"> <div className="messagelogger-edited">
{parseEditContent(edit.content, message)} {Parser.parse(edit.content)}
<Timestamp <Timestamp
timestamp={edit.timestamp} timestamp={edit.timestamp}
isEdited={true} isEdited={true}
@ -209,21 +191,11 @@ export default definePlugin({
description: "Whether to log deleted messages", description: "Whether to log deleted messages",
default: true, default: true,
}, },
collapseDeleted: {
type: OptionType.BOOLEAN,
description: "Whether to collapse deleted messages, similar to blocked messages",
default: false
},
logEdits: { logEdits: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Whether to log edited messages", description: "Whether to log edited messages",
default: true, default: true,
}, },
inlineEdits: {
type: OptionType.BOOLEAN,
description: "Whether to display edit history as part of message content",
default: true
},
ignoreBots: { ignoreBots: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Whether to ignore messages by bots", description: "Whether to ignore messages by bots",
@ -299,23 +271,6 @@ export default definePlugin({
(message.channel_id === "1026515880080842772" && message.author?.id === "1017176847865352332"); (message.channel_id === "1026515880080842772" && message.author?.id === "1017176847865352332");
}, },
EditMarker({ message, className, children, ...props }: any) {
return (
<span
{...props}
className={classes("messagelogger-edit-marker", className)}
onClick={() => openHistoryModal(message)}
aria-role="button"
>
{children}
</span>
);
},
Messages: proxyLazy(() => ({
DELETED_MESSAGE_COUNT: getMessage("{count, plural, =0 {No deleted messages} one {{count} deleted message} other {{count} deleted messages}}")
})),
patches: [ patches: [
{ {
// MessageStore // MessageStore
@ -369,8 +324,7 @@ export default definePlugin({
match: /this\.customRenderedContent=(\i)\.customRenderedContent,/, match: /this\.customRenderedContent=(\i)\.customRenderedContent,/,
replace: "this.customRenderedContent = $1.customRenderedContent," + replace: "this.customRenderedContent = $1.customRenderedContent," +
"this.deleted = $1.deleted || false," + "this.deleted = $1.deleted || false," +
"this.editHistory = $1.editHistory || []," + "this.editHistory = $1.editHistory || [],"
"this.firstEditTimestamp = $1.firstEditTimestamp || this.editedTimestamp || this.timestamp,"
} }
] ]
}, },
@ -383,7 +337,7 @@ export default definePlugin({
// Pass through editHistory & deleted & original attachments to the "edited message" transformer // Pass through editHistory & deleted & original attachments to the "edited message" transformer
match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/, match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
replace: replace:
"Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, firstEditTimestamp:$1.firstEditTimestamp })" "Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory })"
}, },
{ {
@ -402,8 +356,7 @@ export default definePlugin({
" return $2;" + " return $2;" +
"})())," + "})())," +
"deleted: arguments[1]?.deleted," + "deleted: arguments[1]?.deleted," +
"editHistory: arguments[1]?.editHistory," + "editHistory: arguments[1]?.editHistory"
"firstEditTimestamp: new Date(arguments[1]?.firstEditTimestamp ?? $2.editedTimestamp ?? $2.timestamp)"
}, },
{ {
// Preserve deleted attribute on attachments // Preserve deleted attribute on attachments
@ -451,11 +404,6 @@ export default definePlugin({
// Render editHistory in the deepest div for message content // Render editHistory in the deepest div for message content
match: /(\)\("div",\{id:.+?children:\[)/, match: /(\)\("div",\{id:.+?children:\[)/,
replace: "$1 (!!arguments[0].message.editHistory?.length && $self.renderEdits(arguments[0]))," replace: "$1 (!!arguments[0].message.editHistory?.length && $self.renderEdits(arguments[0])),"
},
{
// Make edit marker clickable
match: /"span",\{(?=className:\i\.edited,)/,
replace: "$self.EditMarker,{message:arguments[0].message,"
} }
] ]
}, },
@ -485,30 +433,6 @@ export default definePlugin({
replace: "children:arguments[0].message.deleted?[]:$1" replace: "children:arguments[0].message.deleted?[]:$1"
} }
] ]
},
{
// Message grouping
find: "NON_COLLAPSIBLE.has(",
replacement: {
match: /if\((\i)\.blocked\)return \i\.\i\.MESSAGE_GROUP_BLOCKED;/,
replace: '$&else if($1.deleted) return"MESSAGE_GROUP_DELETED";',
},
predicate: () => Settings.plugins.MessageLogger.collapseDeleted
},
{
// Message group rendering
find: "Messages.NEW_MESSAGES_ESTIMATED_WITH_DATE",
replacement: [
{
match: /(\i).type===\i\.\i\.MESSAGE_GROUP_BLOCKED\|\|/,
replace: '$&$1.type==="MESSAGE_GROUP_DELETED"||',
},
{
match: /(\i).type===\i\.\i\.MESSAGE_GROUP_BLOCKED\?.*?:/,
replace: '$&$1.type==="MESSAGE_GROUP_DELETED"?$self.Messages.DELETED_MESSAGE_COUNT:',
},
],
predicate: () => Settings.plugins.MessageLogger.collapseDeleted
} }
] ]
}); });

View file

@ -38,17 +38,3 @@
.theme-light .messagelogger-edited { .theme-light .messagelogger-edited {
opacity: 0.5; opacity: 0.5;
} }
.messagelogger-edit-marker {
cursor: pointer;
}
.vc-ml-modal-timestamp {
cursor: unset;
height: unset;
}
.vc-ml-modal-tab-bar {
flex-wrap: wrap;
gap: 16px;
}

View file

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

View file

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

View file

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

View file

@ -1,23 +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";
export default definePlugin({
name: "NoMaskedUrlPaste",
authors: [Devs.CatNoir],
description: "Pasting a link while having text selected will not paste as masked URL",
patches: [
{
find: ".selection,preventEmojiSurrogates:",
replacement: {
match: /if\(null!=\i.selection&&\i.\i.isExpanded\(\i.selection\)\)/,
replace: "if(false)"
}
}
],
});

View file

@ -62,7 +62,16 @@ export default definePlugin({
replace: "return 0;" replace: "return 0;"
} }
}, },
// Message requests hook // New message requests hook
{
find: 'location:"use-message-requests-count"',
predicate: () => settings.store.hideMessageRequestsCount,
replacement: {
match: /getNonChannelAckId\(\i\.\i\.MESSAGE_REQUESTS\).+?return /,
replace: "$&0;"
}
},
// Old message requests hook
{ {
find: "getMessageRequestsCount(){", find: "getMessageRequestsCount(){",
predicate: () => settings.store.hideMessageRequestsCount, predicate: () => settings.store.hideMessageRequestsCount,

View file

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

View file

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

View file

@ -1,11 +0,0 @@
# OpenInApp
Open links in their respective apps instead of your browser
## Currently supports:
- Spotify
- Steam
- EpicGames
- Tidal
- Apple Music (iTunes)

View file

@ -18,70 +18,46 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType, PluginNative, SettingsDefinition } from "@utils/types"; import definePlugin, { OptionType, PluginNative } from "@utils/types";
import { showToast, Toasts } from "@webpack/common"; import { showToast, Toasts } from "@webpack/common";
import type { MouseEvent } from "react"; import type { MouseEvent } from "react";
interface URLReplacementRule { const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
match: RegExp; const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
replace: (...matches: string[]) => string; const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
description: string; const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
shortlinkMatch?: RegExp; const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/;
accountViewReplace?: (userId: string) => string;
}
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant const settings = definePluginSettings({
const UrlReplacementRules: Record<string, URLReplacementRule> = {
spotify: { spotify: {
match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/, type: OptionType.BOOLEAN,
replace: (_, type, id) => `spotify://${type}/${id}`,
description: "Open Spotify links in the Spotify app", description: "Open Spotify links in the Spotify app",
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/, default: true,
accountViewReplace: userId => `spotify:user:${userId}`,
}, },
steam: { steam: {
match: /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/, type: OptionType.BOOLEAN,
replace: match => `steam://openurl/${match}`,
description: "Open Steam links in the Steam app", description: "Open Steam links in the Steam app",
shortlinkMatch: /^https:\/\/s.team\/.+$/, default: true,
accountViewReplace: userId => `steam://openurl/https://steamcommunity.com/profiles/${userId}`,
}, },
epic: { epic: {
match: /^https:\/\/store\.epicgames\.com\/(.+)$/, type: OptionType.BOOLEAN,
replace: (_, id) => `com.epicgames.launcher://store/${id}`,
description: "Open Epic Games links in the Epic Games Launcher", description: "Open Epic Games links in the Epic Games Launcher",
default: true,
}, },
tidal: { tidal: {
match: /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/, type: OptionType.BOOLEAN,
replace: (_, type, id) => `tidal://${type}/${id}`,
description: "Open Tidal links in the Tidal app", description: "Open Tidal links in the Tidal app",
}, default: true,
itunes: { }
match: /^https:\/\/music\.apple\.com\/([a-z]{2}\/)?(album|artist|playlist|song|curator)\/([^/?#]+)\/?([^/?#]+)?(?:\?.*)?(?:#.*)?$/, });
replace: (_, lang, type, name, id) => id ? `itunes://music.apple.com/us/${type}/${name}/${id}` : `itunes://music.apple.com/us/${type}/${name}`,
description: "Open Apple Music links in the iTunes app"
},
};
const pluginSettings = definePluginSettings(
Object.entries(UrlReplacementRules).reduce((acc, [key, rule]) => {
acc[key] = {
type: OptionType.BOOLEAN,
description: rule.description,
default: true,
};
return acc;
}, {} as SettingsDefinition)
);
const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof import("./native")>; const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof import("./native")>;
export default definePlugin({ export default definePlugin({
name: "OpenInApp", name: "OpenInApp",
description: "Open links in their respective apps instead of your browser", description: "Open Spotify, Tidal, Steam and Epic Games URLs in their respective apps instead of your browser",
authors: [Devs.Ven, Devs.surgedevs], authors: [Devs.Ven],
settings: pluginSettings, settings,
patches: [ patches: [
{ {
@ -94,7 +70,7 @@ export default definePlugin({
// Make Spotify profile activity links open in app on web // Make Spotify profile activity links open in app on web
{ {
find: "WEB_OPEN(", find: "WEB_OPEN(",
predicate: () => !IS_DISCORD_DESKTOP && pluginSettings.store.spotify, predicate: () => !IS_DISCORD_DESKTOP && settings.store.spotify,
replacement: { replacement: {
match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g, match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g,
replace: "true$1VencordNative.native.openExternal" replace: "true$1VencordNative.native.openExternal"
@ -103,8 +79,8 @@ export default definePlugin({
{ {
find: ".CONNECTED_ACCOUNT_VIEWED,", find: ".CONNECTED_ACCOUNT_VIEWED,",
replacement: { replacement: {
match: /(?<=href:\i,onClick:(\i)=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/, match: /(?<=href:\i,onClick:\i=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/,
replace: "if($self.handleAccountView($1,$2.type,$2.id)) return;" replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);"
} }
} }
], ],
@ -113,25 +89,61 @@ export default definePlugin({
if (!data) return false; if (!data) return false;
let url = data.href; let url = data.href;
if (!url) return false; if (!IS_WEB && ShortUrlMatcher.test(url)) {
event?.preventDefault();
// CORS jumpscare
url = await Native.resolveRedirect(url);
}
for (const [key, rule] of Object.entries(UrlReplacementRules)) { spotify: {
if (!pluginSettings.store[key]) continue; if (!settings.store.spotify) break spotify;
if (rule.shortlinkMatch?.test(url)) { const match = SpotifyMatcher.exec(url);
event?.preventDefault(); if (!match) break spotify;
url = await Native.resolveRedirect(url);
}
if (rule.match.test(url)) { const [, type, id] = match;
showToast("Opened link in native app", Toasts.Type.SUCCESS); VencordNative.native.openExternal(`spotify:${type}:${id}`);
const newUrl = url.replace(rule.match, rule.replace); event?.preventDefault();
VencordNative.native.openExternal(newUrl); return true;
}
event?.preventDefault(); steam: {
return true; if (!settings.store.steam) break steam;
}
if (!SteamMatcher.test(url)) break steam;
VencordNative.native.openExternal(`steam://openurl/${url}`);
event?.preventDefault();
// Steam does not focus itself so show a toast so it's slightly less confusing
showToast("Opened link in Steam", Toasts.Type.SUCCESS);
return true;
}
epic: {
if (!settings.store.epic) break epic;
const match = EpicMatcher.exec(url);
if (!match) break epic;
VencordNative.native.openExternal(`com.epicgames.launcher://store/${match[1]}`);
event?.preventDefault();
return true;
}
tidal: {
if (!settings.store.tidal) break tidal;
const match = TidalMatcher.exec(url);
if (!match) break tidal;
const [, type, id] = match;
VencordNative.native.openExternal(`tidal://${type}/${id}`);
event?.preventDefault();
return true;
} }
// in case short url didn't end up being something we can handle // in case short url didn't end up being something we can handle
@ -143,12 +155,14 @@ export default definePlugin({
return false; return false;
}, },
handleAccountView(e: MouseEvent, platformType: string, userId: string) { handleAccountView(event: { preventDefault(): void; }, platformType: string, userId: string) {
const rule = UrlReplacementRules[platformType]; if (platformType === "spotify" && settings.store.spotify) {
if (rule?.accountViewReplace && pluginSettings.store[platformType]) { VencordNative.native.openExternal(`spotify:user:${userId}`);
VencordNative.native.openExternal(rule.accountViewReplace(userId)); event.preventDefault();
e.preventDefault(); } else if (platformType === "steam" && settings.store.steam) {
return true; VencordNative.native.openExternal(`steam://openurl/https://steamcommunity.com/profiles/${userId}`);
showToast("Opened link in Steam", Toasts.Type.SUCCESS);
event.preventDefault();
} }
} }
}); });

View file

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

View file

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

View file

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

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