Merge branch 'dev' into main

This commit is contained in:
byeoon 2024-08-17 12:30:20 -04:00 committed by GitHub
commit e7faf3321e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 2637 additions and 1679 deletions

View file

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

View file

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

View file

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

126
eslint.config.mjs Normal file
View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,10 +25,9 @@ import { openPluginModal } from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes"; import type { UserThemeHeader } from "@main/themes";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { showItemInFolder } from "@utils/native"; import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack"; import { findLazy } from "@webpack";
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react"; import type { ComponentType, Ref, SyntheticEvent } from "react";
@ -45,9 +44,7 @@ type FileInput = ComponentType<{
filters?: { name?: string; extensions: string[]; }[]; filters?: { name?: string; extensions: string[]; }[];
}>; }>;
const InviteActions = findByPropsLazy("resolveInvite");
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef); const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
const cl = classNameFactory("vc-settings-theme-"); const cl = classNameFactory("vc-settings-theme-");
@ -306,7 +303,7 @@ function ThemesTab() {
<TextArea <TextArea
value={themeText} value={themeText}
onChange={setThemeText} onChange={setThemeText}
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")} className={"vc-settings-theme-links"}
placeholder="Theme Links" placeholder="Theme Links"
spellCheck={false} spellCheck={false}
onBlur={onBlur} onBlur={onBlur}

View file

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

View file

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

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { get } from "@main/utils/simpleGet";
import { IpcEvents } from "@shared/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
@ -25,7 +26,6 @@ import { join } from "path";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import gitRemote from "~git-remote"; import gitRemote from "~git-remote";
import { get } from "../utils/simpleGet";
import { serializeErrors, VENCORD_FILES } from "./common"; import { serializeErrors, VENCORD_FILES } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`; const API_BASE = `https://api.github.com/repos/${gitRemote}`;

View file

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

1
src/modules.d.ts vendored
View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// eslint-disable-next-line spaced-comment
/// <reference types="standalone-electron-types"/> /// <reference types="standalone-electron-types"/>
declare module "~plugins" { declare module "~plugins" {

View file

@ -91,7 +91,7 @@ export default definePlugin({
/* new profiles */ /* new profiles */
{ {
find: ".PANEL]:14", find: ".FULL_SIZE]:26",
replacement: { replacement: {
match: /(?<=(\i)=\(0,\i\.\i\)\(\i\);)return 0===\i.length\?/, match: /(?<=(\i)=\(0,\i\.\i\)\(\i\);)return 0===\i.length\?/,
replace: "$1.unshift(...$self.getBadges(arguments[0].displayProfile));$&" replace: "$1.unshift(...$self.getBadges(arguments[0].displayProfile));$&"

View file

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

View file

@ -249,6 +249,10 @@ export default definePlugin({
dispatchingFoldersClose = false; dispatchingFoldersClose = false;
}); });
} }
},
LOGOUT() {
closeFolders();
} }
}, },

View file

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

View file

@ -30,7 +30,7 @@ function onPickColor(color: number) {
updateColorVars(hexColor); updateColorVars(hexColor);
} }
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE",settings:{useSystemTheme:"system"==='); const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
function setTheme(theme: string) { function setTheme(theme: string) {
saveClientTheme({ theme }); saveClientTheme({ theme });

View file

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

View file

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

View file

@ -24,7 +24,7 @@ import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Emoji } from "@webpack/types"; import type { Emoji } from "@webpack/types";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
@ -818,7 +818,14 @@ export default definePlugin({
if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false; if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false;
if (this.canUseEmotes) let isUsableTwitchSubEmote = false;
if (e.managed && e.guildId) {
// @ts-ignore outdated type
const myRoles = GuildMemberStore.getSelfMember(e.guildId)?.roles ?? [];
isUsableTwitchSubEmote = e.roles.some(r => myRoles.includes(r));
}
if (this.canUseEmotes || isUsableTwitchSubEmote)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId); return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
else else
return !e.animated && e.guildId === this.guildId; return !e.animated && e.guildId === this.guildId;

View file

@ -57,7 +57,7 @@ function decode(bio: string): Array<number> | null {
if (bio == null) return null; if (bio == null) return null;
const colorString = bio.match( const colorString = bio.match(
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u, /\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e005d}/u,
); );
if (colorString != null) { if (colorString != null) {
const parsed = [...colorString[0]] const parsed = [...colorString[0]]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
# MentionAvatars
Shows user avatars inside mentions
![](https://github.com/user-attachments/assets/fc76ea47-5e19-4063-a592-c57785a75cc7)

View file

@ -0,0 +1,62 @@
/*
* 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 { 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",
default: true
}
});
export default definePlugin({
name: "MentionAvatars",
description: "Shows user avatars inside mentions",
authors: [Devs.Ven],
patches: [{
find: ".USER_MENTION)",
replacement: {
match: /children:"@"\.concat\((null!=\i\?\i:\i)\)(?<=\.useName\((\i)\).+?)/,
replace: "children:$self.renderUsername({username:$1,user:$2})"
}
}],
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-avatar" />
{getUsernameString(username)}
</span>
);
}, { noop: true })
});
function getUsernameString(username: string) {
return settings.store.showAtSymbol
? `@${username}`
: username;
}

View file

@ -0,0 +1,8 @@
.vc-mentionAvatars-avatar {
vertical-align: middle;
width: 1em !important; /* insane discord sets width: 100% in channel topic */
height: 1em;
margin: 0 4px 0.2rem 2px;
border-radius: 50%;
box-sizing: border-box;
}

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards"; import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common"; import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common";
import { Channel, User } from "discord-types/general"; import { Channel, User } from "discord-types/general";
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel"); const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
@ -39,6 +39,19 @@ function getGroupDMName(channel: Channel) {
.join(", "); .join(", ");
} }
const getMutualGroupDms = (userId: string) =>
ChannelStore.getSortedPrivateChannels()
.filter(c => c.isGroupDM() && c.recipients.includes(userId));
const isBotOrSelf = (user: User) => user.bot || user.id === UserStore.getCurrentUser().id;
function getMutualGDMCountText(user: User) {
const count = getMutualGroupDms(user.id).length;
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
}
const IS_PATCHED = Symbol("MutualGroupDMs.Patched");
export default definePlugin({ export default definePlugin({
name: "MutualGroupDMs", name: "MutualGroupDMs",
description: "Shows mutual group dms in profiles", description: "Shows mutual group dms in profiles",
@ -49,7 +62,7 @@ export default definePlugin({
find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded
replacement: { replacement: {
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/, 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"}),' replace: '$self.isBotOrSelf(arguments[0].user)?null:$1"MUTUAL_GDMS",children:$self.getMutualGDMCountText(arguments[0].user)}),'
} }
}, },
{ {
@ -63,8 +76,8 @@ export default definePlugin({
find: ".MUTUAL_FRIENDS?(", find: ".MUTUAL_FRIENDS?(",
replacement: [ replacement: [
{ {
match: /(?<=onItemSelect:\i,children:)(\i)\.map/, match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:'Mutual Groups'}])].map" replace: "$self.pushSection($1, arguments[0].user);$&"
}, },
{ {
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/, match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
@ -74,10 +87,23 @@ export default definePlugin({
} }
], ],
isBotOrSelf: (user: User) => user.bot || user.id === UserStore.getCurrentUser().id, isBotOrSelf,
getMutualGDMCountText,
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; }) => { renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => ( const mutualDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
const entries = mutualDms.map(c => (
<Clickable <Clickable
className={ProfileListClasses.listRow} className={ProfileListClasses.listRow}
onClick={() => { onClick={() => {

View file

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

View file

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

View file

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

View file

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

View file

@ -60,7 +60,7 @@ export default definePlugin({
find: 'location:"UserMention', find: 'location:"UserMention',
replacement: [ replacement: [
{ {
match: /user:(\i),channel:(\i).{0,400}?"@"\.concat\(.+?\)/, match: /onContextMenu:\i,color:\i,\.\.\.\i(?=,children:)(?<=user:(\i),channel:(\i).{0,500}?)/,
replace: "$&,color:$self.getUserColor($1?.id,{channelId:$2?.id})" replace: "$&,color:$self.getUserColor($1?.id,{channelId:$2?.id})"
} }
], ],

View file

@ -26,8 +26,7 @@
} }
.vc-st-modal-header { .vc-st-modal-header {
justify-content: space-between; place-content: center space-between;
align-content: center;
} }
.vc-st-modal-header h1 { .vc-st-modal-header h1 {

View file

@ -211,9 +211,9 @@ export default definePlugin({
} }
}, },
{ {
find: /\.BITE_SIZE,onOpenProfile:\i,usernameIcon:/, find: '"BiteSizeProfileBody"',
replacement: { replacement: {
match: /currentUser:\i,guild:\i,onOpenProfile:.+?}\)(?=])(?<=user:(\i),bio:null==(\i)\?.+?)/, match: /currentUser:\i,guild:\i}\)(?<=user:(\i),bio:null==(\i)\?.+?)/,
replace: "$&,$self.profilePopoutComponent({ user: $1, displayProfile: $2, simplified: true })" replace: "$&,$self.profilePopoutComponent({ user: $1, displayProfile: $2, simplified: true })"
} }
} }

View file

@ -307,11 +307,11 @@ export default definePlugin({
] ]
}, },
{ {
find: '+1]})},"overflow"))', find: '})},"overflow"))',
replacement: [ replacement: [
{ {
// Create a variable for the channel prop // Create a variable for the channel prop
match: /maxUsers:\i,users:\i.+?}=(\i).*?;/, match: /users:\i,maxUsers:\i.+?}=(\i).*?;/,
replace: (m, props) => `${m}let{shcChannel}=${props};` replace: (m, props) => `${m}let{shcChannel}=${props};`
}, },
{ {

View file

@ -18,34 +18,21 @@
import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType, PluginSettingDef } from "@utils/types";
const opt = (description: string) => ({
type: OptionType.BOOLEAN,
description,
default: true,
restartNeeded: true
} satisfies PluginSettingDef);
const settings = definePluginSettings({ const settings = definePluginSettings({
showTimeouts: { showTimeouts: opt("Show member timeout icons in chat."),
type: OptionType.BOOLEAN, showInvitesPaused: opt("Show the invites paused tooltip in the server list."),
description: "Show member timeout icons in chat.", showModView: opt("Show the member mod view context menu item in all servers."),
default: true, disableDiscoveryFilters: opt("Disable filters in Server Discovery search that hide servers that don't meet discovery criteria."),
}, disableDisallowedDiscoveryFilters: opt("Disable filters in Server Discovery search that hide NSFW & disallowed servers."),
showInvitesPaused: {
type: OptionType.BOOLEAN,
description: "Show the invites paused tooltip in the server list.",
default: true,
},
showModView: {
type: OptionType.BOOLEAN,
description: "Show the member mod view context menu item in all servers.",
default: true,
},
disableDiscoveryFilters: {
type: OptionType.BOOLEAN,
description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.",
default: true,
},
disableDisallowedDiscoveryFilters: {
type: OptionType.BOOLEAN,
description: "Disable filters in Server Discovery search that hide NSFW & disallowed servers.",
default: true,
},
}); });
migratePluginSettings("ShowHiddenThings", "ShowTimeouts"); migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
@ -64,7 +51,7 @@ export default definePlugin({
}, },
}, },
{ {
find: "2022-07_invites_disabled", find: "INVITES_DISABLED))||",
predicate: () => settings.store.showInvitesPaused, predicate: () => settings.store.showInvitesPaused,
replacement: { replacement: {
match: /\i\.\i\.can\(\i\.\i.MANAGE_GUILD,\i\)/, match: /\i\.\i\.can\(\i\.\i.MANAGE_GUILD,\i\)/,
@ -79,6 +66,15 @@ export default definePlugin({
replace: "return true", replace: "return true",
} }
}, },
// fixes a bug where Members page must be loaded to see highest role, why is Discord depending on MemberSafetyStore.getEnhancedMember for something that can be obtained here?
{
find: "Messages.GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL,allowOverflow",
predicate: () => settings.store.showModView,
replacement: {
match: /(role:)\i(?=,guildId.{0,100}role:(\i\[))/,
replace: "$1$2arguments[0].member.highestRoleId]",
}
},
{ {
find: "prod_discoverable_guilds", find: "prod_discoverable_guilds",
predicate: () => settings.store.disableDiscoveryFilters, predicate: () => settings.store.disableDiscoveryFilters,

View file

@ -67,7 +67,7 @@ export default definePlugin({
const { nick } = author; const { nick } = author;
const prefix = withMentionPrefix ? "@" : ""; const prefix = withMentionPrefix ? "@" : "";
if (isRepliedMessage && !settings.store.inReplies || username === nick.toLowerCase()) if (isRepliedMessage && !settings.store.inReplies || username.toLowerCase() === nick.toLowerCase())
return <>{prefix}{nick}</>; return <>{prefix}{nick}</>;
if (settings.store.mode === "user-nick") if (settings.store.mode === "user-nick")

View file

@ -165,7 +165,6 @@ function SeekBar() {
const [position, setPosition] = useState(storePosition); const [position, setPosition] = useState(storePosition);
// eslint-disable-next-line consistent-return
useEffect(() => { useEffect(() => {
if (isPlaying && !isSettingPosition) { if (isPlaying && !isSettingPosition) {
setPosition(SpotifyStore.position); setPosition(SpotifyStore.position);
@ -358,7 +357,7 @@ export function Player() {
const [shouldHide, setShouldHide] = useState(false); const [shouldHide, setShouldHide] = useState(false);
// Hide player after 5 minutes of inactivity // Hide player after 5 minutes of inactivity
// eslint-disable-next-line consistent-return
React.useEffect(() => { React.useEffect(() => {
setShouldHide(false); setShouldHide(false);
if (!isPlaying) { if (!isPlaying) {

View file

@ -55,6 +55,7 @@ interface PlayerState {
// added by patch // added by patch
actual_repeat: Repeat; actual_repeat: Repeat;
shuffle: boolean;
} }
interface Device { interface Device {
@ -182,6 +183,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
store.isPlaying = e.isPlaying ?? false; store.isPlaying = e.isPlaying ?? false;
store.volume = e.volumePercent ?? 0; store.volume = e.volumePercent ?? 0;
store.repeat = e.actual_repeat || "off"; store.repeat = e.actual_repeat || "off";
store.shuffle = e.shuffle ?? false;
store.position = e.position ?? 0; store.position = e.position ?? 0;
store.isSettingPosition = false; store.isSettingPosition = false;
store.emitChange(); store.emitChange();

View file

@ -48,7 +48,7 @@ export default definePlugin({
}, },
patches: [ patches: [
{ {
find: '"AccountConnected"', find: "this.isCopiedStreakGodlike",
replacement: { replacement: {
// react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah }) // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah })
match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/, match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/,
@ -70,21 +70,20 @@ export default definePlugin({
replace: "false", replace: "false",
}] }]
}, },
// Discord doesn't give you the repeat kind, only a boolean
{ {
find: 'repeat:"off"!==', find: 'repeat:"off"!==',
replacement: { replacement: [
match: /repeat:"off"!==(.{1,3}),/, {
replace: "actual_repeat:$1,$&" // Discord doesn't give you shuffle state and the repeat kind, only a boolean
} match: /repeat:"off"!==(\i),/,
replace: "shuffle:arguments[2]?.shuffle_state??false,actual_repeat:$1,$&"
}, },
{ {
find: "artists.filter",
replacement: {
match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/, match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/,
replace: "" replace: ""
} }
} ]
},
], ],
start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls), start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls),

View file

@ -101,9 +101,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.2rem; padding: 0.2rem;
justify-content: center;
align-items: flex-start; align-items: flex-start;
align-content: flex-start; place-content: flex-start center;
overflow: hidden; overflow: hidden;
} }

View file

@ -17,10 +17,9 @@
*/ */
import { ChatBarButton } from "@api/ChatButtons"; import { ChatBarButton } from "@api/ChatButtons";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { openModal } from "@utils/modal"; import { openModal } from "@utils/modal";
import { Alerts, Forms } from "@webpack/common"; import { Alerts, Forms, Tooltip, useEffect, useState } from "@webpack/common";
import { settings } from "./settings"; import { settings } from "./settings";
import { TranslateModal } from "./TranslateModal"; import { TranslateModal } from "./TranslateModal";
@ -39,9 +38,17 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
); );
} }
export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => { export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]); const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
const [shouldShowTranslateEnabledTooltip, setter] = useState(false);
useEffect(() => {
setShouldShowTranslateEnabledTooltip = setter;
return () => setShouldShowTranslateEnabledTooltip = undefined;
}, []);
if (!isMainChat || !showChatBarButton) return null; if (!isMainChat || !showChatBarButton) return null;
const toggle = () => { const toggle = () => {
@ -52,21 +59,20 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
title: "Vencord Auto-Translate Enabled", title: "Vencord Auto-Translate Enabled",
body: <> body: <>
<Forms.FormText> <Forms.FormText>
You just enabled auto translate (by right clicking the Translate icon). Any message you send will automatically be translated before being sent. You just enabled Auto Translate! Any message <b>will automatically be translated</b> before being sent.
</Forms.FormText>
<Forms.FormText className={Margins.top16}>
If this was an accident, disable it again, or it will change your message content before sending.
</Forms.FormText> </Forms.FormText>
</>, </>,
cancelText: "Disable Auto-Translate", confirmText: "Disable Auto-Translate",
confirmText: "Got it", cancelText: "Got it",
secondaryConfirmText: "Don't show again", secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false, onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false,
onCancel: () => settings.store.autoTranslate = false onConfirm: () => settings.store.autoTranslate = false,
// troll
confirmColor: "vc-notification-log-danger-btn",
}); });
}; };
return ( const button = (
<ChatBarButton <ChatBarButton
tooltip="Open Translate Modal" tooltip="Open Translate Modal"
onClick={e => { onClick={e => {
@ -76,7 +82,7 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
<TranslateModal rootProps={props} /> <TranslateModal rootProps={props} />
)); ));
}} }}
onContextMenu={() => toggle()} onContextMenu={toggle}
buttonProps={{ buttonProps={{
"aria-haspopup": "dialog" "aria-haspopup": "dialog"
}} }}
@ -84,4 +90,13 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
<TranslateIcon className={cl({ "auto-translate": autoTranslate, "chat-button": true })} /> <TranslateIcon className={cl({ "auto-translate": autoTranslate, "chat-button": true })} />
</ChatBarButton> </ChatBarButton>
); );
if (shouldShowTranslateEnabledTooltip && settings.store.showAutoTranslateTooltip)
return (
<Tooltip text="Auto Translate Enabled" forceOpen>
{() => button}
</Tooltip>
);
return button;
}; };

View file

@ -20,9 +20,8 @@ import { Margins } from "@utils/margins";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { Forms, SearchableSelect, Switch, useMemo } from "@webpack/common"; import { Forms, SearchableSelect, Switch, useMemo } from "@webpack/common";
import { Languages } from "./languages";
import { settings } from "./settings"; import { settings } from "./settings";
import { cl } from "./utils"; import { cl, getLanguages } from "./utils";
const LanguageSettingKeys = ["receivedInput", "receivedOutput", "sentInput", "sentOutput"] as const; const LanguageSettingKeys = ["receivedInput", "receivedOutput", "sentInput", "sentOutput"] as const;
@ -31,7 +30,7 @@ function LanguageSelect({ settingsKey, includeAuto }: { settingsKey: typeof Lang
const options = useMemo( const options = useMemo(
() => { () => {
const options = Object.entries(Languages).map(([value, label]) => ({ value, label })); const options = Object.entries(getLanguages()).map(([value, label]) => ({ value, label }));
if (!includeAuto) if (!includeAuto)
options.shift(); options.shift();

View file

@ -19,7 +19,6 @@
import { Parser, useEffect, useState } from "@webpack/common"; import { Parser, useEffect, useState } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
import { Languages } from "./languages";
import { TranslateIcon } from "./TranslateIcon"; import { TranslateIcon } from "./TranslateIcon";
import { cl, TranslationValue } from "./utils"; import { cl, TranslationValue } from "./utils";
@ -59,7 +58,7 @@ export function TranslationAccessory({ message }: { message: Message; }) {
<TranslateIcon width={16} height={16} /> <TranslateIcon width={16} height={16} />
{Parser.parse(translation.text)} {Parser.parse(translation.text)}
{" "} {" "}
(translated from {Languages[translation.src] ?? translation.src} - <Dismiss onDismiss={() => setTranslation(undefined)} />) (translated from {translation.sourceLanguage} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
</span> </span>
); );
} }

View file

@ -28,7 +28,7 @@ import definePlugin from "@utils/types";
import { ChannelStore, Menu } from "@webpack/common"; import { ChannelStore, Menu } from "@webpack/common";
import { settings } from "./settings"; import { settings } from "./settings";
import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon"; import { setShouldShowTranslateEnabledTooltip, TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
import { handleTranslate, TranslationAccessory } from "./TranslationAccessory"; import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
import { translate } from "./utils"; import { translate } from "./utils";
@ -53,8 +53,8 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) =>
export default definePlugin({ export default definePlugin({
name: "Translate", name: "Translate",
description: "Translate messages with Google Translate", description: "Translate messages with Google Translate or DeepL",
authors: [Devs.Ven], authors: [Devs.Ven, Devs.AshtonMemer],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
settings, settings,
contextMenus: { contextMenus: {
@ -83,11 +83,18 @@ export default definePlugin({
}; };
}); });
let tooltipTimeout: any;
this.preSend = addPreSendListener(async (_, message) => { this.preSend = addPreSendListener(async (_, message) => {
if (!settings.store.autoTranslate) return; if (!settings.store.autoTranslate) return;
if (!message.content) return; if (!message.content) return;
message.content = (await translate("sent", message.content)).text; setShouldShowTranslateEnabledTooltip?.(true);
clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000);
const trans = await translate("sent", message.content);
message.content = trans.text;
}); });
}, },

View file

@ -31,9 +31,10 @@ copy(Object.fromEntries(
)) ))
*/ */
export type Language = keyof typeof Languages; export type GoogleLanguage = keyof typeof GoogleLanguages;
export type DeeplLanguage = keyof typeof DeeplLanguages;
export const Languages = { export const GoogleLanguages = {
"auto": "Detect language", "auto": "Detect language",
"af": "Afrikaans", "af": "Afrikaans",
"sq": "Albanian", "sq": "Albanian",
@ -169,3 +170,57 @@ export const Languages = {
"yo": "Yoruba", "yo": "Yoruba",
"zu": "Zulu" "zu": "Zulu"
} as const; } as const;
export const DeeplLanguages = {
"": "Detect language",
"ar": "Arabic",
"bg": "Bulgarian",
"zh-hans": "Chinese (Simplified)",
"zh-hant": "Chinese (Traditional)",
"cs": "Czech",
"da": "Danish",
"nl": "Dutch",
"en-us": "English (American)",
"en-gb": "English (British)",
"et": "Estonian",
"fi": "Finnish",
"fr": "French",
"de": "German",
"el": "Greek",
"hu": "Hungarian",
"id": "Indonesian",
"it": "Italian",
"ja": "Japanese",
"ko": "Korean",
"lv": "Latvian",
"lt": "Lithuanian",
"nb": "Norwegian",
"pl": "Polish",
"pt-br": "Portuguese (Brazilian)",
"pt-pt": "Portuguese (European)",
"ro": "Romanian",
"ru": "Russian",
"sk": "Slovak",
"sl": "Slovenian",
"es": "Spanish",
"sv": "Swedish",
"tr": "Turkish",
"uk": "Ukrainian"
} as const;
export function deeplLanguageToGoogleLanguage(language: string) {
switch (language) {
case "": return "auto";
case "nb": return "no";
case "zh-hans": return "zh-CN";
case "zh-hant": return "zh-TW";
case "en-us":
case "en-gb":
return "en";
case "pt-br":
case "pt-pt":
return "pt";
default:
return language;
}
}

View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { IpcMainInvokeEvent } from "electron";
export async function makeDeeplTranslateRequest(_: IpcMainInvokeEvent, pro: boolean, apiKey: string, payload: string) {
const url = pro
? "https://api.deepl.com/v2/translate"
: "https://api-free.deepl.com/v2/translate";
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `DeepL-Auth-Key ${apiKey}`
},
body: payload
});
const data = await res.text();
return { status: res.status, data };
} catch (e) {
return { status: -1, data: String(e) };
}
}

View file

@ -22,38 +22,76 @@ import { OptionType } from "@utils/types";
export const settings = definePluginSettings({ export const settings = definePluginSettings({
receivedInput: { receivedInput: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Input language for received messages", description: "Language that received messages should be translated from",
default: "auto", default: "auto",
hidden: true hidden: true
}, },
receivedOutput: { receivedOutput: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Output language for received messages", description: "Language that received messages should be translated to",
default: "en", default: "en",
hidden: true hidden: true
}, },
sentInput: { sentInput: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Input language for sent messages", description: "Language that your own messages should be translated from",
default: "auto", default: "auto",
hidden: true hidden: true
}, },
sentOutput: { sentOutput: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Output language for sent messages", description: "Language that your own messages should be translated to",
default: "en", default: "en",
hidden: true hidden: true
}, },
showChatBarButton: {
type: OptionType.BOOLEAN,
description: "Show translate button in chat bar",
default: true
},
service: {
type: OptionType.SELECT,
description: IS_WEB ? "Translation service (Not supported on Web!)" : "Translation service",
disabled: () => IS_WEB,
options: [
{ label: "Google Translate", value: "google", default: true },
{ label: "DeepL Free", value: "deepl" },
{ label: "DeepL Pro", value: "deepl-pro" }
] as const,
onChange: resetLanguageDefaults
},
deeplApiKey: {
type: OptionType.STRING,
description: "DeepL API key",
default: "",
placeholder: "Get your API key from https://deepl.com/your-account",
disabled: () => IS_WEB
},
autoTranslate: { autoTranslate: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this", description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
default: false default: false
}, },
showChatBarButton: { showAutoTranslateTooltip: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Show translate button in chat bar", description: "Show a tooltip on the ChatBar button whenever a message is automatically translated",
default: true default: true
} },
}).withPrivateSettings<{ }).withPrivateSettings<{
showAutoTranslateAlert: boolean; showAutoTranslateAlert: boolean;
}>(); }>();
export function resetLanguageDefaults() {
if (IS_WEB || settings.store.service === "google") {
settings.store.receivedInput = "auto";
settings.store.receivedOutput = "en";
settings.store.sentInput = "auto";
settings.store.sentOutput = "en";
} else {
settings.store.receivedInput = "";
settings.store.receivedOutput = "en-us";
settings.store.sentInput = "";
settings.store.sentOutput = "en-us";
}
}

View file

@ -3,8 +3,7 @@
} }
.vc-trans-modal-header { .vc-trans-modal-header {
justify-content: space-between; place-content: center space-between;
align-content: center;
} }
.vc-trans-modal-header h1 { .vc-trans-modal-header h1 {

View file

@ -17,12 +17,18 @@
*/ */
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { onlyOnce } from "@utils/onlyOnce";
import { PluginNative } from "@utils/types";
import { showToast, Toasts } from "@webpack/common";
import { settings } from "./settings"; import { DeeplLanguages, deeplLanguageToGoogleLanguage, GoogleLanguages } from "./languages";
import { resetLanguageDefaults, settings } from "./settings";
export const cl = classNameFactory("vc-trans-"); export const cl = classNameFactory("vc-trans-");
interface TranslationData { const Native = VencordNative.pluginHelpers.Translate as PluginNative<typeof import("./native")>;
interface GoogleData {
src: string; src: string;
sentences: { sentences: {
// 🏳️‍⚧️ // 🏳️‍⚧️
@ -30,15 +36,47 @@ interface TranslationData {
}[]; }[];
} }
interface DeeplData {
translations: {
detected_source_language: string;
text: string;
}[];
}
export interface TranslationValue { export interface TranslationValue {
src: string; sourceLanguage: string;
text: string; text: string;
} }
export async function translate(kind: "received" | "sent", text: string): Promise<TranslationValue> { export const getLanguages = () => IS_WEB || settings.store.service === "google"
const sourceLang = settings.store[kind + "Input"]; ? GoogleLanguages
const targetLang = settings.store[kind + "Output"]; : DeeplLanguages;
export async function translate(kind: "received" | "sent", text: string): Promise<TranslationValue> {
const translate = IS_WEB || settings.store.service === "google"
? googleTranslate
: deeplTranslate;
try {
return await translate(
text,
settings.store[`${kind}Input`],
settings.store[`${kind}Output`]
);
} catch (e) {
const userMessage = typeof e === "string"
? e
: "Something went wrong. If this issue persists, please check the console or ask for help in the support server.";
showToast(userMessage, Toasts.Type.FAILURE);
throw e instanceof Error
? e
: new Error(userMessage);
}
}
async function googleTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
const url = "https://translate.googleapis.com/translate_a/single?" + new URLSearchParams({ const url = "https://translate.googleapis.com/translate_a/single?" + new URLSearchParams({
// see https://stackoverflow.com/a/29537590 for more params // see https://stackoverflow.com/a/29537590 for more params
// holy shidd nvidia // holy shidd nvidia
@ -63,13 +101,69 @@ export async function translate(kind: "received" | "sent", text: string): Promis
+ `\n${res.status} ${res.statusText}` + `\n${res.status} ${res.statusText}`
); );
const { src, sentences }: TranslationData = await res.json(); const { src, sentences }: GoogleData = await res.json();
return { return {
src, sourceLanguage: GoogleLanguages[src] ?? src,
text: sentences. text: sentences.
map(s => s?.trans). map(s => s?.trans).
filter(Boolean). filter(Boolean).
join("") join("")
}; };
} }
function fallbackToGoogle(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
return googleTranslate(
text,
deeplLanguageToGoogleLanguage(sourceLang),
deeplLanguageToGoogleLanguage(targetLang)
);
}
const showDeeplApiQuotaToast = onlyOnce(
() => showToast("Deepl API quota exceeded. Falling back to Google Translate", Toasts.Type.FAILURE)
);
async function deeplTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
if (!settings.store.deeplApiKey) {
showToast("DeepL API key is not set. Resetting to Google", Toasts.Type.FAILURE);
settings.store.service = "google";
resetLanguageDefaults();
return fallbackToGoogle(text, sourceLang, targetLang);
}
// CORS jumpscare
const { status, data } = await Native.makeDeeplTranslateRequest(
settings.store.service === "deepl-pro",
settings.store.deeplApiKey,
JSON.stringify({
text: [text],
target_lang: targetLang,
source_lang: sourceLang.split("-")[0]
})
);
switch (status) {
case 200:
break;
case -1:
throw "Failed to connect to DeepL API: " + data;
case 403:
throw "Invalid DeepL API key or version";
case 456:
showDeeplApiQuotaToast();
return fallbackToGoogle(text, sourceLang, targetLang);
default:
throw new Error(`Failed to translate "${text}" (${sourceLang} -> ${targetLang})\n${status} ${data}`);
}
const { translations }: DeeplData = JSON.parse(data);
const src = translations[0].detected_source_language;
return {
sourceLanguage: DeeplLanguages[src] ?? src,
text: translations[0].text
};
}

View file

@ -186,12 +186,11 @@ export default definePlugin({
// Avatar component used in User DMs "User Profile" popup in the right and Profiles Modal pfp // Avatar component used in User DMs "User Profile" popup in the right and Profiles Modal pfp
{ {
find: ".overlay:void 0,status:", find: ".overlay:void 0,status:",
replacement: [ replacement: {
...[/"PRESS_VIEW_PROFILE".+?(?=children:)(?<=avatarSrc:(\i).+?)/, /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/].map(match => ({ match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/,
match,
replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openImage($1)}," replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openImage($1)},"
})) },
] all: true
}, },
// Old Profiles Modal pfp // Old Profiles Modal pfp
{ {

View file

@ -55,6 +55,7 @@ function cleanMessage(msg: Message) {
const cloneAny = clone as any; const cloneAny = clone as any;
delete cloneAny.editHistory; delete cloneAny.editHistory;
delete cloneAny.deleted; delete cloneAny.deleted;
delete cloneAny.firstEditTimestamp;
cloneAny.attachments?.forEach(a => delete a.deleted); cloneAny.attachments?.forEach(a => delete a.deleted);
return clone; return clone;
@ -152,6 +153,7 @@ export default definePlugin({
contextMenus: { contextMenus: {
"guild-context": MakeContextCallback("Guild"), "guild-context": MakeContextCallback("Guild"),
"channel-context": MakeContextCallback("Channel"), "channel-context": MakeContextCallback("Channel"),
"thread-context": MakeContextCallback("Channel"),
"user-context": MakeContextCallback("User") "user-context": MakeContextCallback("User")
}, },

View file

@ -43,14 +43,23 @@ function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
}, },
oldFormErrors: true oldFormErrors: true
}) })
.then(res => FluxDispatcher.dispatch({ .then(res => {
for (const user of res.body) {
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user
});
}
FluxDispatcher.dispatch({
type: "MESSAGE_REACTION_ADD_USERS", type: "MESSAGE_REACTION_ADD_USERS",
channelId: msg.channel_id, channelId: msg.channel_id,
messageId: msg.id, messageId: msg.id,
users: res.body, users: res.body,
emoji, emoji,
reactionType: type reactionType: type
})) });
})
.catch(console.error) .catch(console.error)
.finally(() => sleep(250)); .finally(() => sleep(250));
} }
@ -148,13 +157,6 @@ export default definePlugin({
const reactions = getReactionsWithQueue(message, emoji, type); const reactions = getReactionsWithQueue(message, emoji, type);
const users = Object.values(reactions).filter(Boolean) as User[]; const users = Object.values(reactions).filter(Boolean) as User[];
for (const user of users) {
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user
});
}
return ( return (
<div <div
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }} style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}

View file

@ -1,15 +0,0 @@
# XSOverlay Notifier
Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR.
## Preview
![](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40)
![](https://github.com/Vendicated/Vencord/assets/24845294/f15eff61-2d52-4620-bcab-808ecb1606d2)
## Usage
- Enable this plugin
- Set plugin settings as desired
- Open XSOverlay
- get ping spammed

View file

@ -0,0 +1,14 @@
# XSOverlay Notifier
Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR.
## Preview
![Resulting notification inside XSOverlay](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40)
![Test notification inside XSOverlay](https://github.com/user-attachments/assets/d3b0c387-1d67-4697-a470-d4a927e228f4)
## Usage
- Enable this plugin
- Set port and plugin settings as desired (defaults should work fine)
- Open SteamVR and XSOverlay

View file

@ -10,7 +10,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types"; import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack"; import { findByCodeLazy, findLazy } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common"; import { Button, ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general"; import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
const ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10); const ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10);
@ -68,10 +68,46 @@ interface Call {
ringing: string[]; ringing: string[];
} }
interface ApiObject {
sender: string,
target: string,
command: string,
jsonData: string,
rawData: string | null,
}
interface NotificationObject {
type: number;
timeout: number;
height: number;
opacity: number;
volume: number;
audioPath: string;
title: string;
content: string;
useBase64Icon: boolean;
icon: ArrayBuffer | string;
sourceApp: string;
}
const notificationsShouldNotify = findByCodeLazy(".SUPPRESS_NOTIFICATIONS))return!1"); const notificationsShouldNotify = findByCodeLazy(".SUPPRESS_NOTIFICATIONS))return!1");
const XSLog = new Logger("XSOverlay"); const logger = new Logger("XSOverlay");
const settings = definePluginSettings({ const settings = definePluginSettings({
webSocketPort: {
type: OptionType.NUMBER,
description: "Websocket port",
default: 42070,
async onChange() {
await start();
}
},
preferUDP: {
type: OptionType.BOOLEAN,
description: "Enable if you use an older build of XSOverlay unable to connect through websockets. This setting is ignored on web.",
default: false,
disabled: () => IS_WEB
},
botNotifications: { botNotifications: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Allow bot notifications", description: "Allow bot notifications",
@ -136,6 +172,18 @@ const settings = definePluginSettings({
}, },
}); });
let socket: WebSocket;
async function start() {
if (socket) socket.close();
socket = new WebSocket(`ws://127.0.0.1:${settings.store.webSocketPort ?? 42070}/?client=Vencord`);
return new Promise((resolve, reject) => {
socket.onopen = resolve;
socket.onerror = reject;
setTimeout(reject, 3000);
});
}
const Native = VencordNative.pluginHelpers.XSOverlay as PluginNative<typeof import("./native")>; const Native = VencordNative.pluginHelpers.XSOverlay as PluginNative<typeof import("./native")>;
export default definePlugin({ export default definePlugin({
@ -248,7 +296,21 @@ export default definePlugin({
if (shouldIgnoreForChannelType(channel)) return; if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message); sendMsgNotif(titleString, finalMsg, message);
} }
} },
start,
stop() {
socket.close();
},
settingsAboutComponent: () => (
<>
<Button onClick={() => sendOtherNotif("This is a test notification! explode", "Hello from Vendor!")}>
Send test notification
</Button>
</>
)
}); });
function shouldIgnoreForChannelType(channel: Channel) { function shouldIgnoreForChannelType(channel: Channel) {
@ -259,9 +321,8 @@ function shouldIgnoreForChannelType(channel: Channel) {
function sendMsgNotif(titleString: string, content: string, message: Message) { function sendMsgNotif(titleString: string, content: string, message: Message) {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => { fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
const msgData = { const msgData: NotificationObject = {
messageType: 1, type: 1,
index: 0,
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout, timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
height: calculateHeight(content), height: calculateHeight(content),
opacity: settings.store.opacity, opacity: settings.store.opacity,
@ -270,17 +331,17 @@ function sendMsgNotif(titleString: string, content: string, message: Message) {
title: titleString, title: titleString,
content: content, content: content,
useBase64Icon: true, useBase64Icon: true,
icon: result, icon: new TextDecoder().decode(result),
sourceApp: "Vencord" sourceApp: "Vencord"
}; };
Native.sendToOverlay(msgData);
sendToOverlay(msgData);
}); });
} }
function sendOtherNotif(content: string, titleString: string) { function sendOtherNotif(content: string, titleString: string) {
const msgData = { const msgData: NotificationObject = {
messageType: 1, type: 1,
index: 0,
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout, timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
height: calculateHeight(content), height: calculateHeight(content),
opacity: settings.store.opacity, opacity: settings.store.opacity,
@ -289,10 +350,26 @@ function sendOtherNotif(content: string, titleString: string) {
title: titleString, title: titleString,
content: content, content: content,
useBase64Icon: false, useBase64Icon: false,
icon: null, icon: "default",
sourceApp: "Vencord" sourceApp: "Vencord"
}; };
Native.sendToOverlay(msgData); sendToOverlay(msgData);
}
async function sendToOverlay(notif: NotificationObject) {
if (!IS_WEB && settings.store.preferUDP) {
Native.sendToOverlay(notif);
return;
}
const apiObject: ApiObject = {
sender: "Vencord",
target: "xsoverlay",
command: "SendNotification",
jsonData: JSON.stringify(notif),
rawData: null
};
if (socket.readyState !== socket.OPEN) await start();
socket.send(JSON.stringify(apiObject));
} }
function shouldNotify(message: Message, channel: string) { function shouldNotify(message: Message, channel: string) {

View file

@ -9,7 +9,7 @@ import { createSocket, Socket } from "dgram";
let xsoSocket: Socket; let xsoSocket: Socket;
export function sendToOverlay(_, data: any) { export function sendToOverlay(_, data: any) {
data.icon = Buffer.from(data.icon).toString("base64"); data.messageType = data.type;
const json = JSON.stringify(data); const json = JSON.stringify(data);
xsoSocket ??= createSocket("udp4"); xsoSocket ??= createSocket("udp4");
xsoSocket.send(json, 42069, "127.0.0.1"); xsoSocket.send(json, 42069, "127.0.0.1");

View file

@ -1,6 +1,7 @@
# WatchTogetherAdblock # WatchTogetherAdblock
Block ads in the YouTube WatchTogether activity via AdGuard Block ads in YouTube embeds and the WatchTogether activity via AdGuard
Note that this only works for yourself, other users in the activity will still see ads. Note that this only works for yourself, other users in the activity will still see ads.
Powered by a modified version of [Adguard's BlockYoutubeAdsShortcut](https://github.com/AdguardTeam/BlockYouTubeAdsShortcut) Powered by a modified version of [Adguard's BlockYoutubeAdsShortcut](https://github.com/AdguardTeam/BlockYouTubeAdsShortcut)

View file

@ -19,7 +19,6 @@
* along with AdGuard's Block YouTube Ads. If not, see <http://www.gnu.org/licenses/>. * along with AdGuard's Block YouTube Ads. If not, see <http://www.gnu.org/licenses/>.
*/ */
const LOGO_ID = "block-youtube-ads-logo";
const hiddenCSS = [ const hiddenCSS = [
"#__ffYoutube1", "#__ffYoutube1",
"#__ffYoutube2", "#__ffYoutube2",
@ -98,7 +97,7 @@ const hideElements = () => {
} }
const rule = selectors.join(", ") + " { display: none!important; }"; const rule = selectors.join(", ") + " { display: none!important; }";
const style = document.createElement("style"); const style = document.createElement("style");
style.innerHTML = rule; style.textContent = rule;
document.head.appendChild(style); document.head.appendChild(style);
}; };
/** /**
@ -165,11 +164,9 @@ const overrideObject = (obj, propertyName, overrideValue) => {
} }
let overriden = false; let overriden = false;
for (const key in obj) { for (const key in obj) {
// eslint-disable-next-line no-prototype-builtins
if (obj.hasOwnProperty(key) && key === propertyName) { if (obj.hasOwnProperty(key) && key === propertyName) {
obj[key] = overrideValue; obj[key] = overrideValue;
overriden = true; overriden = true;
// eslint-disable-next-line no-prototype-builtins
} else if (obj.hasOwnProperty(key) && typeof obj[key] === "object") { } else if (obj.hasOwnProperty(key) && typeof obj[key] === "object") {
if (overrideObject(obj[key], propertyName, overrideValue)) { if (overrideObject(obj[key], propertyName, overrideValue)) {
overriden = true; overriden = true;
@ -195,68 +192,25 @@ const jsonOverride = (propertyName, overrideValue) => {
return obj; return obj;
}; };
// Override Response.prototype.json // Override Response.prototype.json
const nativeResponseJson = Response.prototype.json; Response.prototype.json = new Proxy(Response.prototype.json, {
Response.prototype.json = new Proxy(nativeResponseJson, { async apply(...args) {
apply(...args) {
// Call the target function, get the original Promise // Call the target function, get the original Promise
const promise = Reflect.apply(...args); const result = await Reflect.apply(...args);
// Create a new one and override the JSON inside // Create a new one and override the JSON inside
return new Promise((resolve, reject) => { overrideObject(result, propertyName, overrideValue);
promise.then(data => { return result;
overrideObject(data, propertyName, overrideValue);
resolve(data);
}).catch(error => reject(error));
});
}, },
}); });
}; };
const addAdGuardLogoStyle = () => { };
const addAdGuardLogo = () => {
if (document.getElementById(LOGO_ID)) {
return;
}
const logo = document.createElement("span");
logo.innerHTML = "__logo_text__";
logo.setAttribute("id", LOGO_ID);
if (window.location.hostname === "m.youtube.com") {
const btn = document.querySelector("header.mobile-topbar-header > button");
if (btn) {
btn.parentNode?.insertBefore(logo, btn.nextSibling);
addAdGuardLogoStyle();
}
} else if (window.location.hostname === "www.youtube.com") {
const code = document.getElementById("country-code");
if (code) {
code.innerHTML = "";
code.appendChild(logo);
addAdGuardLogoStyle();
}
} else if (window.location.hostname === "music.youtube.com") {
const el = document.querySelector(".ytmusic-nav-bar#left-content");
if (el) {
el.appendChild(logo);
addAdGuardLogoStyle();
}
} else if (window.location.hostname === "www.youtube-nocookie.com") {
const code = document.querySelector("#yt-masthead #logo-container .content-region");
if (code) {
code.innerHTML = "";
code.appendChild(logo);
addAdGuardLogoStyle();
}
}
};
// Removes ads metadata from YouTube XHR requests // Removes ads metadata from YouTube XHR requests
jsonOverride("adPlacements", []); jsonOverride("adPlacements", []);
jsonOverride("playerAds", []); jsonOverride("playerAds", []);
// Applies CSS that hides YouTube ad elements // Applies CSS that hides YouTube ad elements
hideElements(); hideElements();
// Some changes should be re-evaluated on every page change // Some changes should be re-evaluated on every page change
addAdGuardLogo();
hideDynamicAds(); hideDynamicAds();
autoSkipAds(); autoSkipAds();
observeDomChanges(() => { observeDomChanges(() => {
addAdGuardLogo();
hideDynamicAds(); hideDynamicAds();
autoSkipAds(); autoSkipAds();
}); });

View file

@ -4,12 +4,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
// The entire code of this plugin can be found in native.ts // The entire code of this plugin can be found in native.ts
migratePluginSettings("YoutubeAdblock", "WatchTogetherAdblock");
export default definePlugin({ export default definePlugin({
name: "WatchTogetherAdblock", name: "YoutubeAdblock",
description: "Block ads in the YouTube WatchTogether activity via AdGuard", description: "Block ads in YouTube embeds and the WatchTogether activity via AdGuard",
authors: [Devs.ImLvna], authors: [Devs.ImLvna, Devs.Ven],
}); });

View file

@ -11,9 +11,9 @@ import adguard from "file://adguard.js?minify";
app.on("browser-window-created", (_, win) => { app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => { win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => { frame.once("dom-ready", () => {
if (frame.url.includes("discordsays") && frame.url.includes("youtube.com")) { if (!RendererSettings.store.plugins?.YoutubeAdblock?.enabled) return;
if (!RendererSettings.store.plugins?.WatchTogetherAdblock?.enabled) return;
if (frame.url.includes("youtube.com/embed/") || (frame.url.includes("discordsays") && frame.url.includes("youtube.com"))) {
frame.executeJavaScript(adguard); frame.executeJavaScript(adguard);
} }
}); });

View file

@ -33,10 +33,6 @@ export interface Dev {
* If you are fine with attribution but don't want the badge, add badge: false * If you are fine with attribution but don't want the badge, add badge: false
*/ */
export const Devs = /* #__PURE__*/ Object.freeze({ export const Devs = /* #__PURE__*/ Object.freeze({
Nobody: {
name: "Nobody",
id: 0n,
},
Ven: { Ven: {
name: "Vee", name: "Vee",
id: 343383572805058560n id: 343383572805058560n
@ -533,7 +529,23 @@ export const Devs = /* #__PURE__*/ Object.freeze({
Antti: { Antti: {
name: "Antti", name: "Antti",
id: 312974985876471810n id: 312974985876471810n
} },
Joona: {
name: "Joona",
id: 297410829589020673n
},
AshtonMemer: {
name: "AshtonMemer",
id: 373657230530052099n
},
surgedevs: {
name: "Chloe",
id: 1084592643784331324n
},
Lumap: {
name: "Lumap",
id: 585278686291427338n,
},
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// eslint-disable-next-line path-alias/no-relative
import { filters, findByPropsLazy, waitFor } from "@webpack"; import { filters, findByPropsLazy, waitFor } from "@webpack";
import { waitForComponent } from "./internal"; import { waitForComponent } from "./internal";

View file

@ -66,7 +66,6 @@ export let DraftStore: t.DraftStore;
* *
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id); * @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/ */
// eslint-disable-next-line prefer-destructuring
export const useStateFromStores: t.useStateFromStores = findByCodeLazy("useStateFromStores"); export const useStateFromStores: t.useStateFromStores = findByCodeLazy("useStateFromStores");
waitForStore("DraftStore", s => DraftStore = s); waitForStore("DraftStore", s => DraftStore = s);

View file

@ -299,7 +299,7 @@ export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "f
* Note that the example below exists already as an api, see {@link findByPropsLazy} * Note that the example below exists already as an api, see {@link findByPropsLazy}
* @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah);
*/ */
export function proxyLazyWebpack<T = any>(factory: () => any, attempts?: number) { export function proxyLazyWebpack<T = any>(factory: () => T, attempts?: number) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]);
return proxyLazy<T>(factory, attempts); return proxyLazy<T>(factory, attempts);
@ -544,7 +544,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
} }
if (rawChunkIds) { if (rawChunkIds) {
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]); const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => Number(m[1]));
await Promise.all(chunkIds.map(id => wreq.e(id))); await Promise.all(chunkIds.map(id => wreq.e(id)));
} }
@ -559,7 +559,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
return false; return false;
} }
wreq(entryPointId); wreq(Number(entryPointId));
return true; return true;
} }

View file

@ -4,6 +4,7 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"allowJs": true,
"lib": [ "lib": [
"DOM", "DOM",
"DOM.Iterable", "DOM.Iterable",
@ -37,7 +38,8 @@
"transform": "typescript-transform-paths", "transform": "typescript-transform-paths",
"afterDeclarations": true "afterDeclarations": true
} }
] ],
"outDir": "who-fucking-cares-dude"
}, },
"include": ["src/**/*", "browser/**/*", "scripts/**/*"] "include": ["src/**/*", "browser/**/*", "scripts/**/*", "eslint.config.mjs"],
} }