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*
*.tsbuildinfo
src/userplugins
ExtensionCache/
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",
"rules": {
"indentation": 4,
"selector-class-pattern": [
"^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$",
{

View file

@ -4,6 +4,7 @@
"EditorConfig.EditorConfig",
"GregorBiswanger.json2ts",
"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",
"javascript.preferences.quoteStyle": "double",
"eslint.experimental.useFlatConfig": false,
"gitlens.remotes": [
{
"domain": "codeberg.org",
"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",
"private": "true",
"version": "1.10.1",
"version": "1.9.3",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -21,13 +21,12 @@
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
"buildReporterDesktop": "pnpm build --reporter",
"watch": "pnpm build --watch",
"dev": "pnpm watch",
"watchWeb": "pnpm buildWeb --watch",
"generatePluginJson": "tsx scripts/generatePluginList.ts",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "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:fix": "pnpm lint --fix",
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
@ -35,54 +34,54 @@
"testTsc": "tsc --noEmit"
},
"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/shiki": "0.10.5",
"fflate": "^0.8.2",
"eslint-plugin-simple-header": "^1.0.2",
"fflate": "^0.7.4",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.50.0",
"nanoid": "^5.0.7",
"nanoid": "^4.0.2",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^2.6.1",
"@types/chrome": "^0.0.269",
"@types/diff": "^5.2.1",
"@types/lodash": "^4.17.7",
"@types/node": "^22.0.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/yazl": "^2.4.5",
"diff": "^5.2.0",
"@types/chrome": "^0.0.246",
"@types/diff": "^5.0.3",
"@types/lodash": "^4.14.194",
"@types/node": "^18.16.3",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"diff": "^5.1.0",
"discord-types": "^1.3.26",
"esbuild": "^0.15.18",
"eslint": "^9.8.0",
"eslint": "^8.46.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "2.1.0",
"eslint-plugin-simple-header": "^1.1.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.0.1",
"highlight.js": "10.7.3",
"eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0",
"html-minifier-terser": "^7.2.0",
"moment": "^2.30.1",
"puppeteer-core": "^22.15.0",
"moment": "^2.29.4",
"puppeteer-core": "^19.11.1",
"standalone-electron-types": "^1.0.0",
"stylelint": "^16.8.1",
"stylelint-config-standard": "^36.0.1",
"ts-patch": "^3.2.1",
"ts-pattern": "^5.3.1",
"tsx": "^4.16.5",
"type-fest": "^4.23.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.0",
"stylelint": "^15.6.0",
"stylelint-config-standard": "^33.0.0",
"ts-patch": "^3.1.2",
"tsx": "^3.12.7",
"type-fest": "^3.9.0",
"typescript": "^5.4.5",
"typescript-transform-paths": "^3.4.7",
"zip-local": "^0.3.5"
},
"packageManager": "pnpm@9.1.0",
"pnpm": {
"patchedDependencies": {
"eslint@9.8.0": "patches/eslint@9.8.0.patch",
"eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch"
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
"eslint@8.46.0": "patches/eslint@8.46.0.patch"
},
"peerDependencyRules": {
"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 { 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 = {
IS_STANDALONE,
@ -131,7 +131,7 @@ await Promise.all([
sourcemap,
plugins: [
globPlugins("discordDesktop"),
...commonRendererPlugins
...commonOpts.plugins
],
define: {
...defines,
@ -180,7 +180,7 @@ await Promise.all([
sourcemap,
plugins: [
globPlugins("vencordDesktop"),
...commonRendererPlugins
...commonOpts.plugins
],
define: {
...defines,

View file

@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
import { join } from "path";
import Zip from "zip-local";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins } from "./common.mjs";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
@ -36,7 +36,7 @@ const commonOptions = {
external: ["~plugins", "~git-hash", "/assets/*"],
plugins: [
globPlugins("web"),
...commonRendererPlugins
...commonOpts.plugins,
],
target: ["esnext"],
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[]>}

View file

@ -28,7 +28,6 @@ import { join, relative } from "path";
import { promisify } from "util";
import { getPluginTarget } from "../utils.mjs";
import { builtinModules } from "module";
/** @type {import("../../package.json")} */
const PackageJSON = JSON.parse(readFileSync("package.json"));
@ -294,16 +293,37 @@ export const stylePlugin = {
};
/**
* @type {(filter: RegExp, message: string) => import("esbuild").Plugin}
* @type {import("esbuild").Plugin}
*/
export const banImportPlugin = (filter, message) => ({
name: "ban-imports",
setup: build => {
build.onResolve({ filter }, () => {
return { errors: [{ text: message }] };
export const translationPlugin = {
name: "translation-plugin",
setup: ({ onResolve, onLoad }) => {
const filter = /^~translations$/;
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}
@ -316,24 +336,11 @@ export const commonOpts = {
sourcemap: watch ? "inline" : "",
legalComments: "linked",
banner,
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin, translationPlugin],
external: ["~plugins", "~git-hash", "~git-remote", "~translations", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",
// Work around https://github.com/evanw/esbuild/issues/2460
tsconfig: "./scripts/build/tsconfig.esbuild.json"
};
const escapedBuiltinModules = builtinModules
.map(m => m.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
.join("|");
const builtinModuleRegex = new RegExp(`^(node:)?(${escapedBuiltinModules})$`);
export const commonRendererPlugins = [
banImportPlugin(builtinModuleRegex, "Cannot import node inbuilt modules in browser code. You need to use a native.ts file"),
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
...commonOpts.plugins
];

View file

@ -20,7 +20,7 @@ import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
import { access, readFile } from "fs/promises";
import { join, sep } from "path";
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";
@ -90,6 +90,38 @@ function parseDevs() {
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) {
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
@ -120,10 +152,16 @@ async function parseFile(fileName: string) {
switch (key) {
case "name":
case "description":
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
data[key] = value.text;
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":
data.hasPatches = true;
break;

View file

@ -36,7 +36,7 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({
headless: true,
headless: "new",
executablePath: process.env.CHROMIUM_BIN
});

View file

@ -38,6 +38,7 @@ import { patches, PMLogger, startAllPlugins } from "./plugins";
import { localStorage } from "./utils/localStorage";
import { relaunch } from "./utils/native";
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
import { t } from "./utils/translation";
import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
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
showNotification({
title: "Cloud Integrations",
body: "We've noticed you have cloud integrations enabled in another client! Due to limitations, you will " +
"need to re-authenticate to continue using them. Click here to go to the settings page to do so!",
title: t("vencord.cloudIntegrations"),
body: t("vencord.cloud.integrations.reauthenticate"),
color: "var(--yellow-360)",
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
// of the possible ones it has (such as when your settings are newer).
showNotification({
title: "Cloud Settings",
body: "Your settings have been updated! Click here to restart to fully apply changes!",
title: t("vencord.cloudSettings"),
body: t("vencord.cloud.settings.updated"),
color: "var(--green-360)",
onClick: relaunch
});
@ -100,8 +100,8 @@ async function init() {
await update();
if (Settings.autoUpdateNotification)
setTimeout(() => showNotification({
title: "Vencord has been updated!",
body: "Click here to restart",
title: t("vencord.update.updated"),
body: t("vencord.update.clickToRestart"),
permanent: true,
noPersist: true,
onClick: relaunch
@ -110,8 +110,8 @@ async function init() {
}
setTimeout(() => showNotification({
title: "A Vencord update is available!",
body: "Click here to view the update",
title: t("vencord.update.available"),
body: t("vencord.update.clickToView"),
permanent: true,
noPersist: true,
onClick: openUpdaterModal!

View file

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

View file

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

View file

@ -16,17 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { Channel, Message } from "discord-types/general";
import type { ComponentType, MouseEventHandler } from "react";
import type { MouseEventHandler } from "react";
const logger = new Logger("MessagePopover");
export interface ButtonItem {
key?: string,
label: string,
icon: ComponentType<any>,
icon: React.ComponentType<any>,
message: Message,
channel: Channel,
onClick?: MouseEventHandler<HTMLButtonElement>,
@ -49,26 +48,22 @@ export function removeButton(identifier: string) {
}
export function _buildPopoverElements(
Component: React.ComponentType<ButtonItem>,
message: Message
msg: Message,
makeButton: (item: ButtonItem) => React.ComponentType
) {
const items: React.ReactNode[] = [];
const items = [] as React.ComponentType[];
for (const [identifier, getItem] of buttons.entries()) {
try {
const item = getItem(message);
const item = getItem(msg);
if (item) {
item.key ??= identifier;
items.push(
<ErrorBoundary noop>
<Component {...item} />
</ErrorBoundary>
);
items.push(makeButton(item));
}
} catch (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");
return Settings.plugins[definedSettings.pluginName] as any;
},
get plain() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return PlainSettings.plugins[definedSettings.pluginName] as any;
},
use: settings => useSettings(
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
).plugins[definedSettings.pluginName] as any,

View file

@ -19,6 +19,7 @@
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/react";
import { t } from "@utils/translation";
import { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard";
@ -85,11 +86,11 @@ const ErrorBoundary = LazyComponent(() => {
{...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 (
<ErrorCard style={{ overflow: "hidden" }}>
<h1>Oh no!</h1>
<h1>{t("vencord.ohNo")}</h1>
<p>{msg}</p>
<code>
{this.state.message}

View file

@ -1,6 +1,7 @@
.vc-expandableheader-center-flex {
display: flex;
place-items: center;
justify-items: center;
align-items: center;
}
.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) {
return (
@ -75,9 +76,8 @@ export function CopyIcon(props: IconProps) {
viewBox="0 0 24 24"
>
<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="M6 18a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4h-3a5 5 0 0 1-5-5V6h-4a4 4 0 0 0-4 4v8Z" />
<path d="M21.73 12a3 3 0 0 0-.6-.88l-4.25-4.24a3 3 0 0 0-.88-.61V9a3 3 0 0 0 3 3h2.73Z" />
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" />
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" />
</g>
</Icon>
);

View file

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

View file

@ -28,6 +28,7 @@ import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { t } from "@utils/translation";
import { OptionType, Plugin } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
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() {
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 {
const options = Object.entries(plugin.options).map(([key, setting]) => {
if (setting.hidden) return null;
@ -274,7 +275,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</div>
)}
<Forms.FormSection className={Margins.bottom16}>
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
<Forms.FormTitle tag="h3">{t("vencord.settings")}</Forms.FormTitle>
{renderSettings()}
</Forms.FormSection>
</ModalContent>
@ -289,7 +290,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
>
Cancel
</Button>
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
<Tooltip text={t("vencord.settingsErrors")} shouldShow={!canSubmit()}>
{({ onMouseEnter, onMouseLeave }) => (
<Button
size={Button.Sizes.SMALL}
@ -299,12 +300,12 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
onMouseLeave={onMouseLeave}
disabled={!canSubmit()}
>
Save & Close
{t("vencord.saveAndClose")}
</Button>
)}
</Tooltip>
</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>
</ModalFooter>}
</ModalRoot>

View file

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

View file

@ -21,6 +21,7 @@ import "./addonCard.css";
import { classNameFactory } from "@api/Styles";
import { Badge } from "@components/Badge";
import { Switch } from "@components/Switch";
import { t } from "@utils/translation";
import { Text, useRef } from "@webpack/common";
import type { MouseEventHandler, ReactNode } from "react";
@ -67,7 +68,7 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
>
{name}
</div>
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
</div>{isNew && <Badge text={t("vencord.new")} color="#ED4245" />}
</Text>
{!!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 { classes } from "@utils/misc";
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
import { t } from "@utils/translation";
import { Button, Card, Text } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
@ -29,21 +30,19 @@ function BackupRestoreTab() {
<SettingsTab title="Backup & Restore">
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
<Flex flexDirection="column">
<strong>Warning</strong>
<span>Importing a settings file will overwrite your current settings.</span>
<strong>{t("vencord.warning")}</strong>
<span>{t("vencord.backupAndRestore.importWarning")}</span>
</Flex>
</Card>
<Text variant="text-md/normal" className={Margins.bottom8}>
You can import and export your Vencord settings as a JSON file.
This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord.
{t("vencord.backupAndRestore.description")}
</Text>
<Text variant="text-md/normal" className={Margins.bottom8}>
Settings Export contains:
{t("vencord.backupAndRestore.exportContains")}
<ul>
<li>&mdash; Custom QuickCSS</li>
<li>&mdash; Theme Links</li>
<li>&mdash; Plugin Settings</li>
<li>&mdash; {t("vencord.backupAndRestore.customQuickcss")}</li>
<li>&mdash; {t("vencord.backupAndRestore.themeLinks")}</li>
<li>&mdash; {t("vencord.backupAndRestore.pluginSettings")}</li>
</ul>
</Text>
<Flex>
@ -51,13 +50,13 @@ function BackupRestoreTab() {
onClick={() => uploadSettingsBackup()}
size={Button.Sizes.SMALL}
>
Import Settings
{t("vencord.backupAndRestore.importSettings")}
</Button>
<Button
onClick={downloadSettingsBackup}
size={Button.Sizes.SMALL}
>
Export Settings
{t("vencord.backupAndRestore.exportSettings")}
</Button>
</Flex>
</SettingsTab>

View file

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

View file

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

View file

@ -16,7 +16,7 @@
* 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 { Flex } from "@components/Flex";
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 { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { 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 type { ComponentType, Ref, SyntheticEvent } from "react";
import Plugins from "~plugins";
import { AddonCard } from "./AddonCard";
import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared";
@ -44,7 +44,9 @@ type FileInput = ComponentType<{
filters?: { name?: string; extensions: string[]; }[];
}>;
const InviteActions = findByPropsLazy("resolveInvite");
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
const cl = classNameFactory("vc-settings-theme-");
@ -77,16 +79,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div>
{themeLinks.map(rawLink => {
const { label, link } = (() => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return { label: rawLink, link: rawLink };
const [, mode, link] = match;
return { label: `[${mode} mode only] ${link}`, link };
})();
return <Card style={{
{themeLinks.map(link => (
<Card style={{
padding: ".5em",
marginBottom: ".5em",
marginTop: ".5em"
@ -94,11 +88,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
}}>
{label}
{link}
</Forms.FormTitle>
<Validator link={link} />
</Card>;
})}
</Card>
))}
</div>
</>
);
@ -209,60 +203,68 @@ function ThemesTab() {
return (
<>
<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" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
BetterDiscord Themes
{t("vencord.themes.betterDiscord")}
</Link>
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
</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>
<Forms.FormSection title="Local Themes">
<QuickActionCard>
<Forms.FormSection title={t("vencord.themes.local")}>
<Card className="vc-settings-quick-actions-card">
<>
{IS_WEB ?
(
<QuickAction
text={
<span style={{ position: "relative" }}>
Upload Theme
<Button
size={Button.Sizes.SMALL}
disabled={themeDirPending}
>
{t("vencord.themes.upload")}
<FileInput
ref={fileInputRef}
onChange={onFileUpload}
multiple={true}
filters={[{ extensions: ["css"] }]}
/>
</span>
}
Icon={PlusIcon}
/>
</Button>
) : (
<QuickAction
text="Open Themes Folder"
text={t("vencord.themes.openFolder")}
action={() => showItemInFolder(themeDir!)}
disabled={themeDirPending}
Icon={FolderIcon}
/>
>
{t("vencord.themes.openFolder")}
</Button>
)}
<QuickAction
text="Load missing Themes"
action={refreshLocalThemes}
Icon={RestartIcon}
/>
<QuickAction
text="Edit QuickCSS"
action={() => VencordNative.quickCss.openEditor()}
Icon={PaintbrushIcon}
/>
<Button
onClick={refreshLocalThemes}
size={Button.Sizes.SMALL}
>
{t("vencord.themes.loadMissing")}
</Button>
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
>
{t("vencord.themes.editQuickCss")}
</Button>
{Settings.plugins.ClientTheme.enabled && (
<QuickAction
text="Edit ClientTheme"
action={() => openPluginModal(Plugins.ClientTheme)}
Icon={PencilIcon}
{Vencord.Settings.plugins.ClientTheme.enabled && (
<Button
onClick={() => openModal(modalProps => (
<PluginModal
{...modalProps}
plugin={Vencord.Plugins.plugins.ClientTheme}
onRestartNeeded={() => { }}
/>
))}
size={Button.Sizes.SMALL}
>
{t("clientTheme.edit")}
</Button>
)}
</>
</QuickActionCard>
@ -302,17 +304,16 @@ function ThemesTab() {
return (
<>
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
<Forms.FormTitle tag="h5">{t("vencord.themes.pasteLinks")}</Forms.FormTitle>
<Forms.FormText>{t("vencord.themes.oneLinkPerLine")}</Forms.FormText>
<Forms.FormText>{t("vencord.themes.useDirect")}</Forms.FormText>
</Card>
<Forms.FormSection title="Online Themes" tag="h5">
<Forms.FormSection title={t("vencord.themes.online")} tag="h5">
<TextArea
value={themeText}
onChange={setThemeText}
className={"vc-settings-theme-links"}
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")}
placeholder="Theme Links"
spellCheck={false}
onBlur={onBlur}
@ -337,13 +338,13 @@ function ThemesTab() {
className="vc-settings-tab-bar-item"
id={ThemeTab.LOCAL}
>
Local Themes
{t("vencord.themes.local")}
</TabBar.Item>
<TabBar.Item
className="vc-settings-tab-bar-item"
id={ThemeTab.ONLINE}
>
Online Themes
{t("vencord.themes.online")}
</TabBar.Item>
</TabBar>

View file

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

View file

@ -16,12 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { t } from "@utils/translation";
import { maybePromptToUpdate } from "@utils/updater";
export function handleComponentFailed() {
maybePromptToUpdate(
"Uh Oh! Failed to render this Page." +
" However, there is an update available that might fix it." +
" Would you like to update and restart now?"
t("vencord.failureUpdate")
);
}

View file

@ -15,9 +15,9 @@ export async function loadLazyChunks() {
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
const validChunks = new Set<number>();
const invalidChunks = new Set<number>();
const deferredRequires = new Set<number>();
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
const deferredRequires = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
@ -29,14 +29,14 @@ export async function loadLazyChunks() {
async function searchAndLoadLazyChunks(factoryCode: string) {
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
// the chunk containing the component
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
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) {
return;
@ -61,7 +61,7 @@ export async function loadLazyChunks() {
}
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
const allChunks = [] as number[];
const allChunks = [] as string[];
// 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];
if (id == null) continue;
allChunks.push(Number(id));
allChunks.push(id);
}
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/>.
*/
import { get } from "@main/utils/simpleGet";
import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron";
@ -26,6 +25,7 @@ import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import { get } from "../utils/simpleGet";
import { serializeErrors, VENCORD_FILES } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;

View file

@ -35,8 +35,7 @@ export const ALLOWED_PROTOCOLS = [
"steam:",
"spotify:",
"com.epicgames.launcher:",
"tidal:",
"itunes:",
"tidal:"
];
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/>.
*/
// eslint-disable-next-line spaced-comment
/// <reference types="standalone-electron-types"/>
declare module "~plugins" {
@ -42,6 +43,11 @@ declare module "~git-remote" {
export default remote;
}
declare module "~translations" {
const translations: Record<string, Record<string, any>>;
export default translations;
}
declare module "file://*" {
const content: string;
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/>.
*/
import "./fixBadgeOverflow.css";
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
@ -60,8 +62,36 @@ export default definePlugin({
authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
required: true,
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: {
match: /(?<=(\i)=\(0,\i\.\i\)\(\i\);)return 0===\i.length\?/,
replace: "$1.unshift(...$self.getBadges(arguments[0].displayProfile));$&"
@ -77,7 +107,7 @@ export default definePlugin({
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 }) :"
},
// conditionally override their onClick with badge.onClick if it exists

View file

@ -26,8 +26,13 @@ export default definePlugin({
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.Messages\.MESSAGE_ACTION_REPLY.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/,
replace: "$&,Vencord.Api.MessagePopover._buildPopoverElements($1,$2)"
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
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",
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($&)"
}
}

View file

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

View file

@ -64,7 +64,7 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
},
{
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,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})`
}
]

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

View file

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

View file

@ -16,31 +16,19 @@
* 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 { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
const settings = definePluginSettings({
hide: {
type: OptionType.BOOLEAN,
description: "Hide notes",
default: false,
restartNeeded: true
},
noSpellCheck: {
type: OptionType.BOOLEAN,
description: "Disable spellcheck in notes",
disabled: () => Settings.plugins.BetterNotesBox.hide,
default: false
}
});
const UserPopoutSectionCssClasses = findByPropsLazy("section", "lastSection");
export default definePlugin({
name: "BetterNotesBox",
description: "Hide notes or disable spellcheck (Configure in settings!!)",
authors: [Devs.Ven],
settings,
patches: [
{
@ -48,7 +36,7 @@ export default definePlugin({
all: true,
// Some modules match the find but the replacement is returned untouched
noWarn: true,
predicate: () => settings.store.hide,
predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide,
replacement: {
match: /hideNote:.+?(?=([,}].*?\)))/g,
replace: (m, rest) => {
@ -66,12 +54,37 @@ export default definePlugin({
find: "Messages.NOTE_PLACEHOLDER",
replacement: {
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() {
return settings.store.noSpellCheck;
options: {
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",
patches: [
{
find: '"ChannelAttachButton"',
find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE",
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,",
},
},

View file

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

View file

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

View file

@ -26,11 +26,12 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
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";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
@ -435,8 +436,8 @@ export default definePlugin({
<Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle, padding: 8, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
{activity[0] && <ActivityComponent activity={activity[0]} channelId={SelectedChannelStore.getChannelId()}
<div style={{ width: "284px", ...profileThemeStyle }}>
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
application={{ id: settings.store.appID }}
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>) {
try {
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;
@ -63,22 +63,18 @@ async function embedDidMount(this: Component<Props>) {
if (!hasTitle && !hasThumb) return;
embed.dearrow = {
enabled: dearrowByDefault
enabled: true
};
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
const replacementTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
embed.dearrow.oldTitle = dearrowByDefault ? embed.rawTitle : replacementTitle;
if (dearrowByDefault) embed.rawTitle = replacementTitle;
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
}
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 (dearrowByDefault) embed.thumbnail.proxyURL = replacementProxyURL;
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
}
this.forceUpdate();
@ -100,7 +96,6 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
onClick={() => {
const { enabled, oldThumb, oldTitle } = embed.dearrow;
settings.store.dearrowByDefault = !enabled;
embed.dearrow.enabled = !enabled;
if (oldTitle) {
embed.dearrow.oldTitle = embed.rawTitle;
@ -158,12 +153,6 @@ const settings = definePluginSettings({
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
],
},
dearrowByDefault: {
description: "Dearrow videos automatically",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
}
});

View file

@ -91,7 +91,7 @@ export default definePlugin({
replacement: [
// Use Decor avatar decoration hook
{
match: /(?<=\i\)\({avatarDecoration:)(\i)(?=,)(?<=currentUser:(\i).+?)/,
match: /(?<=\i\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/,
replace: "$self.useUserDecorAvatarDecoration($1)??$&"
}
]

View file

@ -8,7 +8,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { openInviteModal } from "@utils/discord";
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 { findComponentByCodeLazy } from "@webpack";
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[];
}
interface SectionHeaderProps {
section: Section;
}
function SectionHeader({ section }: SectionHeaderProps) {
function SectionHeader({ section }: { section: Section; }) {
const hasSubtitle = typeof section.subtitle !== "undefined";
const hasAuthorIds = typeof section.authorIds !== "undefined";
@ -66,7 +62,6 @@ function SectionHeader({ section }: SectionHeaderProps) {
})();
}, [section.authorIds]);
return <div>
<Flex>
<Forms.FormTitle style={{ flexGrow: 1 }}>{section.title}</Forms.FormTitle>
@ -79,7 +74,8 @@ function SectionHeader({ section }: SectionHeaderProps) {
size={16}
showUserPopout
className={Margins.bottom8}
/>}
/>
}
</Flex>
{hasSubtitle &&
<Forms.FormText type="description" className={Margins.bottom8}>
@ -208,16 +204,7 @@ function ChangeDecorationModal(props: ModalProps) {
{activeSelectedDecoration?.alt}
</Text>
}
{activeDecorationHasAuthor && (
<Text key={`createdBy-${activeSelectedDecoration.authorId}`}>
Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}
</Text>
)}
{isActiveDecorationPreset && (
<Button onClick={() => copyWithToast(activeDecorationPreset.id)}>
Copy Preset ID
</Button>
)}
{activeDecorationHasAuthor && <Text key={`createdBy-${activeSelectedDecoration.authorId}`}>Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}</Text>}
</div>
</ErrorBoundary>
</ModalContent>

View file

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

View file

@ -24,7 +24,7 @@ import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import { 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 { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
@ -818,14 +818,7 @@ export default definePlugin({
if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false;
let isUsableTwitchSubEmote = false;
if (e.managed && e.guildId) {
// @ts-ignore outdated type
const myRoles = GuildMemberStore.getSelfMember(e.guildId)?.roles ?? [];
isUsableTwitchSubEmote = e.roles.some(r => myRoles.includes(r));
}
if (this.canUseEmotes || isUsableTwitchSubEmote)
if (this.canUseEmotes)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
else
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;
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) {
const parsed = [...colorString[0]]
@ -121,7 +121,7 @@ export default definePlugin({
{
find: "UserProfileStore",
replacement: {
match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/,
match: /(?<=getUserProfile\(\i\){return )(\i\[\i\])/,
replace: "$self.colorDecodeHook($1)"
}
},

View file

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

View file

@ -7,47 +7,121 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { RelationshipStore, Text } from "@webpack/common";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { Heading, RelationshipStore, Text } from "@webpack/common";
const containerWrapper = findByPropsLazy("memberSinceWrapper");
const container = findByPropsLazy("memberSince");
const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"');
const locale = findByPropsLazy("getLocale");
const 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({
name: "FriendsSince",
description: "Shows when you became friends with someone in the user popout",
authors: [Devs.Elvyra, Devs.Antti],
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",
replacement: {
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\"",
replacement: {
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;
const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null;
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 ? (
<Text variant="text-sm/normal">
@ -75,7 +149,8 @@ export default definePlugin({
</div>
)
}
</Section>
</section>
);
}, { 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;
}
const enum FilterMode {
Whitelist,
Blacklist
}
const RunningGameStore = findStoreLazy("RunningGameStore");
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);
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
recalculateActivities();
}
function recalculateActivities() {
// Trigger activities recalculation
ShowCurrentGame.updateSetting(old => old);
}
function ImportCustomRPCComponent() {
return (
<Flex flexDirection="column">
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the 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>
<Button
onClick={() => {
@ -94,7 +86,7 @@ function ImportCustomRPCComponent() {
return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
}
const isAlreadyAdded = idsListPushID?.(id);
const isAlreadyAdded = allowedIdsPushID?.(id);
if (isAlreadyAdded) {
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; }) {
const [idsList, setIdsList] = useState<string>(settings.store.idsList ?? "");
function AllowedIdsComponent(props: { setValue: (value: string) => void; }) {
const [allowedIds, setAllowedIds] = useState<string>(settings.store.allowedIds ?? "");
idsListPushID = (id: string) => {
const currentIds = new Set(idsList.split(",").map(id => id.trim()).filter(Boolean));
allowedIdsPushID = (id: string) => {
const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean));
const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);
const ids = Array.from(currentIds).join(", ");
setIdsList(ids);
setAllowedIds(ids);
props.setValue(ids);
return isAlreadyAdded;
};
useEffect(() => () => {
idsListPushID = null;
allowedIdsPushID = null;
}, []);
function handleChange(newValue: string) {
setIdsList(newValue);
setAllowedIds(newValue);
props.setValue(newValue);
}
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText>
<Forms.FormTitle tag="h3">Allowed List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC)</Forms.FormText>
<TextInput
type="text"
value={idsList}
value={allowedIds}
onChange={handleChange}
placeholder="235834946571337729, 343383572805058560"
/>
@ -153,62 +145,40 @@ const settings = definePluginSettings({
description: "",
component: () => <ImportCustomRPCComponent />
},
listMode: {
type: OptionType.SELECT,
description: "Change the mode of the filter list",
options: [
{
label: "Whitelist",
value: FilterMode.Whitelist,
default: true
},
{
label: "Blacklist",
value: FilterMode.Blacklist,
}
],
onChange: recalculateActivities
},
idsList: {
allowedIds: {
type: OptionType.COMPONENT,
description: "",
default: "",
onChange(newValue: string) {
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
settings.store.idsList = Array.from(ids).join(", ");
recalculateActivities();
settings.store.allowedIds = Array.from(ids).join(", ");
},
component: props => <IdsListComponent setValue={props.setValue} />
component: props => <AllowedIdsComponent setValue={props.setValue} />
},
ignorePlaying: {
type: OptionType.BOOLEAN,
description: "Ignore all playing activities (These are usually game and RPC activities)",
default: false,
onChange: recalculateActivities
default: false
},
ignoreStreaming: {
type: OptionType.BOOLEAN,
description: "Ignore all streaming activities",
default: false,
onChange: recalculateActivities
default: false
},
ignoreListening: {
type: OptionType.BOOLEAN,
description: "Ignore all listening activities (These are usually spotify activities)",
default: false,
onChange: recalculateActivities
default: false
},
ignoreWatching: {
type: OptionType.BOOLEAN,
description: "Ignore all watching activities",
default: false,
onChange: recalculateActivities
default: false
},
ignoreCompeting: {
type: OptionType.BOOLEAN,
description: "Ignore all competing activities (These are normally special game activities)",
default: false,
onChange: recalculateActivities
default: false
}
}).withPrivateSettings<{
ignoredActivities: IgnoredActivity[];
@ -219,8 +189,8 @@ function getIgnoredActivities() {
}
function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.idsList.includes(id)) {
return settings.store.listMode === FilterMode.Blacklist;
if (id && settings.store.allowedIds.includes(id)) {
return false;
}
switch (type) {
@ -236,8 +206,8 @@ function isActivityTypeIgnored(type: number, id?: string) {
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz, Devs.Kylie],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below",
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.",
dependencies: ["UserSettingsAPI"],
settings,
@ -266,7 +236,6 @@ export default definePlugin({
replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`
}
},
// Discord has 2 different components for activities. Currently, the last is the one being used
{
find: ".activityTitleText,variant",
replacement: {
@ -275,21 +244,15 @@ export default definePlugin({
},
},
{
find: ".promotedLabelWrapperNonBanner,children",
find: ".activityCardDetails,children",
replacement: {
match: /\.appDetailsHeaderContainer.+?children:\i.*?}\),(?<=application:(\i).+?)/,
match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
}
}
],
async start() {
// Migrate allowedIds
if (Settings.plugins.IgnoreActivities.allowedIds) {
settings.store.idsList = Settings.plugins.IgnoreActivities.allowedIds;
delete Settings.plugins.IgnoreActivities.allowedIds; // Remove allowedIds
}
const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
if (oldIgnoredActivitiesData != null) {
@ -316,7 +279,7 @@ export default definePlugin({
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
if (props.application_id != null) {
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || (settings.store.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 {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) {

View file

@ -171,7 +171,7 @@ export default definePlugin({
find: ".handleImageLoad)",
replacement: [
{
match: /placeholderVersion:\i,(?=.{0,50}children:)/,
match: /placeholderVersion:\i,/,
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.filter(({ predicate }) => !predicate || predicate());
if (IS_REPORTER) {
patch.replacement.forEach(r => {
delete r.predicate;
});
}
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
patches.push(patch);
}

View file

@ -133,12 +133,10 @@ export default definePlugin({
message: message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: async () => {
const res = await iteratePasswords(message);
if (res)
this.buildEmbed(message, res);
else
buildDecModal({ message });
await iteratePasswords(message).then((res: string | false) => {
if (res) return void this.buildEmbed(message, res);
return void buildDecModal({ message });
});
}
}
: null;
@ -171,9 +169,9 @@ export default definePlugin({
message.embeds.push({
type: "rich",
rawTitle: "Decrypted Message",
title: "Decrypted Message",
color: "0x45f5f5",
rawDescription: revealed,
description: revealed,
footer: {
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 { isObjectEmpty } from "@utils/misc";
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";
export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) {
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id;
const guildId = isTooltip ? tooltipGuildId! : currentChannel.guild_id;
const totalCount = useStateFromStores(
[GuildMemberCountStore],
@ -31,19 +30,10 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
() => ChannelMemberStore.getProps(guildId, currentChannel?.id)
);
const threadGroups = useStateFromStores(
[ThreadMemberListStore],
() => ThreadMemberListStore.getMemberListSections(currentChannel?.id)
);
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {
onlineCount = groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0);
}
if (!isTooltip && threadGroups && !isObjectEmpty(threadGroups)) {
onlineCount = Object.values(threadGroups).reduce((total, curr) => total + (curr.sectionId === "offline" ? 0 : curr.userIds.length), 0);
}
useEffect(() => {
OnlineMemberCountStore.ensureCount(guildId);
}, [guildId]);

View file

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

View file

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

View file

@ -1,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;
const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
if (!message) return;
messageCache.set(message.id, {
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 ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, i18n, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, i18n, Menu, MessageStore, Parser, Timestamp, UserStore, useStateFromStores } from "@webpack/common";
import { Message } from "discord-types/general";
import overlayStyle from "./deleteStyleOverlay.css?managed";
import textStyle from "./deleteStyleText.css?managed";
import { openHistoryModal } from "./HistoryModal";
interface MLMessage extends Message {
deleted?: boolean;
editHistory?: { timestamp: Date; content: string; }[];
firstEditTimestamp?: Date;
}
const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessage");
const getMessage = findByCodeLazy('replace(/^\\n+|\\n+$/g,"")');
function addDeleteStyle() {
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({
name: "MessageLogger",
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"],
contextMenus: {
"message": patchMessageContextMenu,
"channel-context": patchChannelContextMenu,
"thread-context": patchChannelContextMenu,
"user-context": patchChannelContextMenu,
"gdm-context": patchChannelContextMenu
},
@ -168,11 +150,11 @@ export default definePlugin({
(oldMsg, newMsg) => oldMsg?.editHistory === newMsg?.editHistory
);
return Settings.plugins.MessageLogger.inlineEdits && (
return (
<>
{message.editHistory?.map(edit => (
<div className="messagelogger-edited">
{parseEditContent(edit.content, message)}
{Parser.parse(edit.content)}
<Timestamp
timestamp={edit.timestamp}
isEdited={true}
@ -209,21 +191,11 @@ export default definePlugin({
description: "Whether to log deleted messages",
default: true,
},
collapseDeleted: {
type: OptionType.BOOLEAN,
description: "Whether to collapse deleted messages, similar to blocked messages",
default: false
},
logEdits: {
type: OptionType.BOOLEAN,
description: "Whether to log edited messages",
default: true,
},
inlineEdits: {
type: OptionType.BOOLEAN,
description: "Whether to display edit history as part of message content",
default: true
},
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by bots",
@ -299,23 +271,6 @@ export default definePlugin({
(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: [
{
// MessageStore
@ -369,8 +324,7 @@ export default definePlugin({
match: /this\.customRenderedContent=(\i)\.customRenderedContent,/,
replace: "this.customRenderedContent = $1.customRenderedContent," +
"this.deleted = $1.deleted || false," +
"this.editHistory = $1.editHistory || []," +
"this.firstEditTimestamp = $1.firstEditTimestamp || this.editedTimestamp || this.timestamp,"
"this.editHistory = $1.editHistory || [],"
}
]
},
@ -383,7 +337,7 @@ export default definePlugin({
// Pass through editHistory & deleted & original attachments to the "edited message" transformer
match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
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;" +
"})())," +
"deleted: arguments[1]?.deleted," +
"editHistory: arguments[1]?.editHistory," +
"firstEditTimestamp: new Date(arguments[1]?.firstEditTimestamp ?? $2.editedTimestamp ?? $2.timestamp)"
"editHistory: arguments[1]?.editHistory"
},
{
// Preserve deleted attribute on attachments
@ -451,11 +404,6 @@ export default definePlugin({
// Render editHistory in the deepest div for message content
match: /(\)\("div",\{id:.+?children:\[)/,
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"
}
]
},
{
// 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 {
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 definePlugin, { OptionType } from "@utils/types";
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 { 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 }])
) as TagSettings;
function SettingsComponent() {
const tagSettings = settings.store.tagSettings ??= defaultSettings;
function SettingsComponent(props: { setValue(v: any): void; }) {
settings.store.tagSettings ??= defaultSettings;
const [tagSettings, setTagSettings] = useState(settings.store.tagSettings as TagSettings);
const setValue = (v: TagSettings) => {
setTagSettings(v);
props.setValue(v);
};
return (
<Flex flexDirection="column">
@ -131,13 +137,19 @@ function SettingsComponent() {
type="text"
value={tagSettings[t.name]?.text ?? 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}
/>
<Switch
value={tagSettings[t.name]?.showInChat ?? true}
onChange={v => tagSettings[t.name].showInChat = v}
onChange={v => {
tagSettings[t.name].showInChat = v;
setValue(tagSettings);
}}
hideBorder
>
Show in messages
@ -145,7 +157,10 @@ function SettingsComponent() {
<Switch
value={tagSettings[t.name]?.showInNotChat ?? true}
onChange={v => tagSettings[t.name].showInNotChat = v}
onChange={v => {
tagSettings[t.name].showInNotChat = v;
setValue(tagSettings);
}}
hideBorder
>
Show in member list and profiles
@ -168,7 +183,7 @@ const settings = definePluginSettings({
tagSettings: {
type: OptionType.COMPONENT,
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: {
match: /(?=,hideBotTag:!0)/,
match: /(?=,botClass:)/,
replace: ",moreTags_channelId:arguments[0].moreTags_channelId"
}
},
// in profiles
{
find: ",overrideDiscriminator:",
group: true,
replacement: [
{
// prevent channel id from getting ghosted
@ -249,7 +263,7 @@ export default definePlugin({
match: /user:\i,nick:\i,/,
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'}),"
}
]

View file

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

View file

@ -18,21 +18,36 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { UserStore } from "@webpack/common";
export default definePlugin({
name: "NoProfileThemes",
description: "Completely removes Nitro profile themes from everyone but yourself",
description: "Completely removes Nitro profile themes",
authors: [Devs.TheKodeToad],
patches: [
{
find: ".NITRO_BANNER,",
replacement: {
// = isPremiumAtLeast(user.premiumType, TIER_2)
match: /=(?=\i\.\i\.isPremiumAtLeast\(null==(\i))/,
// = user.banner && isPremiumAtLeast(user.premiumType, TIER_2)
replace: "=(arguments[0]?.bannerSrc||$1?.banner)&&"
}
},
{
find: ".avatarPositionPremiumNoBanner,default:",
replacement: {
// premiumUserWithoutBanner: foo().avatarPositionPremiumNoBanner, default: foo().avatarPositionNormal
match: /\.avatarPositionPremiumNoBanner(?=,default:\i\.(\i))/,
// premiumUserWithoutBanner: foo().avatarPositionNormal...
replace: ".$1"
}
},
{
find: "hasThemeColors(){",
replacement: {
match: /get canUsePremiumProfileCustomization\(\){return /,
replace: "$&$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) {
if (emoji.type !== 1) {
if (emoji.type !== "GUILD_EMOJI") {
return false;
}
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 { 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 type { MouseEvent } from "react";
interface URLReplacementRule {
match: RegExp;
replace: (...matches: string[]) => string;
description: string;
shortlinkMatch?: RegExp;
accountViewReplace?: (userId: string) => string;
}
const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/;
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant
const UrlReplacementRules: Record<string, URLReplacementRule> = {
const settings = definePluginSettings({
spotify: {
match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/,
replace: (_, type, id) => `spotify://${type}/${id}`,
type: OptionType.BOOLEAN,
description: "Open Spotify links in the Spotify app",
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/,
accountViewReplace: userId => `spotify:user:${userId}`,
default: true,
},
steam: {
match: /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/,
replace: match => `steam://openurl/${match}`,
type: OptionType.BOOLEAN,
description: "Open Steam links in the Steam app",
shortlinkMatch: /^https:\/\/s.team\/.+$/,
accountViewReplace: userId => `steam://openurl/https://steamcommunity.com/profiles/${userId}`,
default: true,
},
epic: {
match: /^https:\/\/store\.epicgames\.com\/(.+)$/,
replace: (_, id) => `com.epicgames.launcher://store/${id}`,
type: OptionType.BOOLEAN,
description: "Open Epic Games links in the Epic Games Launcher",
default: true,
},
tidal: {
match: /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/,
replace: (_, type, id) => `tidal://${type}/${id}`,
description: "Open Tidal links in the Tidal app",
},
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,
description: "Open Tidal links in the Tidal app",
default: true,
};
return acc;
}, {} as SettingsDefinition)
);
}
});
const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof import("./native")>;
export default definePlugin({
name: "OpenInApp",
description: "Open links in their respective apps instead of your browser",
authors: [Devs.Ven, Devs.surgedevs],
settings: pluginSettings,
description: "Open Spotify, Tidal, Steam and Epic Games URLs in their respective apps instead of your browser",
authors: [Devs.Ven],
settings,
patches: [
{
@ -94,7 +70,7 @@ export default definePlugin({
// Make Spotify profile activity links open in app on web
{
find: "WEB_OPEN(",
predicate: () => !IS_DISCORD_DESKTOP && pluginSettings.store.spotify,
predicate: () => !IS_DISCORD_DESKTOP && settings.store.spotify,
replacement: {
match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g,
replace: "true$1VencordNative.native.openExternal"
@ -103,8 +79,8 @@ export default definePlugin({
{
find: ".CONNECTED_ACCOUNT_VIEWED,",
replacement: {
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;"
match: /(?<=href:\i,onClick:\i=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/,
replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);"
}
}
],
@ -113,25 +89,61 @@ export default definePlugin({
if (!data) return false;
let url = data.href;
if (!url) return false;
for (const [key, rule] of Object.entries(UrlReplacementRules)) {
if (!pluginSettings.store[key]) continue;
if (rule.shortlinkMatch?.test(url)) {
if (!IS_WEB && ShortUrlMatcher.test(url)) {
event?.preventDefault();
// CORS jumpscare
url = await Native.resolveRedirect(url);
}
if (rule.match.test(url)) {
showToast("Opened link in native app", Toasts.Type.SUCCESS);
spotify: {
if (!settings.store.spotify) break spotify;
const newUrl = url.replace(rule.match, rule.replace);
VencordNative.native.openExternal(newUrl);
const match = SpotifyMatcher.exec(url);
if (!match) break spotify;
const [, type, id] = match;
VencordNative.native.openExternal(`spotify:${type}:${id}`);
event?.preventDefault();
return true;
}
steam: {
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
@ -143,12 +155,14 @@ export default definePlugin({
return false;
},
handleAccountView(e: MouseEvent, platformType: string, userId: string) {
const rule = UrlReplacementRules[platformType];
if (rule?.accountViewReplace && pluginSettings.store[platformType]) {
VencordNative.native.openExternal(rule.accountViewReplace(userId));
e.preventDefault();
return true;
handleAccountView(event: { preventDefault(): void; }, platformType: string, userId: string) {
if (platformType === "spotify" && settings.store.spotify) {
VencordNative.native.openExternal(`spotify:user:${userId}`);
event.preventDefault();
} else if (platformType === "steam" && settings.store.steam) {
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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findLazy } from "@webpack";
import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
const InvitesDisabledExperiment = findLazy(m => m.definition?.id === "2022-07_invites_disabled");
function showDisableInvites(guildId: string) {
// Once the experiment is removed, this should keep working
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
// @ts-ignore
return !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
}
function disableInvites(guildId: string) {

View file

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

View file

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

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