diff --git a/.eslintrc.json b/.eslintrc.json index 4cb86e0e8..aaaaaeb69 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -37,7 +37,7 @@ " * Vencord, a modification for Discord's desktop app", { "pattern": " \\* Copyright \\(c\\) \\d{4}", - "template": " * Copyright (c) 2022 Vendicated and contributors" + "template": " * Copyright (c) 2023 Vendicated and contributors" }, " *", " * This program is free software: you can redistribute it and/or modify", @@ -82,11 +82,13 @@ "no-constant-condition": ["error", { "checkLoops": false }], "no-duplicate-imports": "error", "no-extra-semi": "error", - "consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }], "dot-notation": "error", - "no-useless-escape": ["error", { - "extra": "i" - }], + "no-useless-escape": [ + "error", + { + "extra": "i" + } + ], "no-fallthrough": "error", "for-direction": "error", "no-async-promise-executor": "error", diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/blank.yml b/.github/ISSUE_TEMPLATE/blank.yml new file mode 100644 index 000000000..9887db99f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/blank.yml @@ -0,0 +1,22 @@ +name: Blank Template +description: Use this only if your issue does not fit into another template. **DO NOT ASK FOR SUPPORT OR REQUEST PLUGINS** +labels: [] + +body: + - type: textarea + id: info-sec + attributes: + label: Tell us all about it. + description: Go nuts, let us know what you're wanting to bring attention to. + placeholder: ... + validations: + required: true + + - type: checkboxes + id: agreement-check + attributes: + label: Request Agreement + description: DO NOT USE THIS TEMPLATE FOR SUPPORT OR PLUGIN REQUESTS!!! For Support, **join our Discord**. For plugin requests, **use discussions** + options: + - label: This is not a support or plugin request + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..734bbaca8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,66 @@ +name: Bug/Crash Report +description: Create a bug or crash report for Vencord +labels: [bug] +title: "[Bug] " + +body: + - type: input + id: discord + attributes: + label: Discord Account + description: Who on Discord is making this request? Not required but encouraged for easier follow-up + placeholder: username#0000 + validations: + required: false + + - type: textarea + id: bug-description + attributes: + label: What happens when the bug or crash occurs? + description: Where does this bug or crash occur, when does it occur, etc. + placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ... + validations: + required: true + + - type: textarea + id: expected-behaviour + attributes: + label: What is the expected behaviour? + description: Simply detail what the expected behaviour is. + placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ... + validations: + required: true + + - type: textarea + id: steps-to-take + attributes: + label: How do you recreate this bug or crash? + description: Give us a list of steps in order to recreate the bug or crash. + placeholder: | + 1. Do ... + 2. Then ... + 3. Do this ..., ... and then ... + 4. Observe "the bug" or "the crash" + validations: + required: true + + - type: textarea + id: crash-log + attributes: + label: Errors + description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```". + value: | + ``` + Replace this text with your crash-log. + ``` + validations: + required: false + + - type: checkboxes + id: agreement-check + attributes: + label: Request Agreement + description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable + options: + - label: I am using Discord Stable or tried on Stable and this bug happens there as well + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..bc5d97662 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Vencord Support Server + url: https://discord.gg/D9uwnFnqmd + about: If you need help regarding Vencord, please join our support server! + - name: Vencord Installer + url: https://github.com/Vencord/Installer + about: You can find the Vencord Installer here diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..115f7f700 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature Request +description: Create a feature request for Vencord. To request new plugins, please use the Discussions tab +labels: [enhancement] +title: "[Feature Request] <title>" + +body: + - type: input + id: discord + attributes: + label: Discord Account + description: Who on Discord is making this request? Not required but encouraged for easier follow-up + placeholder: username#0000 + validations: + required: false + + - type: textarea + id: feature-basic-description + attributes: + label: What is it that you'd like to see? + description: Describe the feature you want added as detailed as possible + placeholder: I think ... would be a cool feature to add. This would be awesome, thanks! + validations: + required: true + + - type: checkboxes + id: agreement-check + attributes: + label: Request Agreement + description: DO NOT USE THIS TEMPLATE FOR PLUGIN REQUESTS!!! For plugin requests, **use discussions** + options: + - label: This is not a plugin request + required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c9eafee4..c264821e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,28 +34,19 @@ jobs: - name: Build web run: pnpm buildWeb --standalone - - name: Sign firefox extension - run: | - pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted - env: - WEBEXT_USER: ${{ secrets.WEBEXT_USER }} - WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }} - - name: Build run: pnpm build --standalone - - name: Rename extensions for more user friendliness + - name: Clean up obsolete files run: | - mv dist/*.xpi dist/Vencord-for-Firefox.xpi - mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip - rm -rf dist/extension-v2-unpacked dist/extension-v2.zip + rm -rf dist/extension* Vencord.user.css - name: Get some values needed for the release id: release_values run: | echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - - name: Upload Devbuild as release + - name: Upload DevBuild as release run: | gh release upload devbuild --clobber dist/* gh release edit devbuild --title "DevBuild $RELEASE_TAG" @@ -63,13 +54,15 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ env.release_tag }} - - name: Upload Devbuild to builds repo + - name: Upload DevBuild to builds repo run: | git config --global user.name "$USERNAME" git config --global user.email actions@github.com git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload cd upload + + GLOBIGNORE=.git:.gitignore:README.md:LICENSE rm -rf * cp -r ../dist/* . @@ -78,6 +71,5 @@ jobs: git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git env: API_TOKEN: ${{ secrets.BUILDS_TOKEN }} - GLOBIGNORE: .git:.gitignore:README.md:LICENSE GH_REPO: Vencord/builds USERNAME: GitHub-Actions diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..89cc2cb8a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,61 @@ +name: Release Browser Extension +on: + push: + tags: + - v* + +jobs: + Publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: check that tag matches package.json version + run: | + pkg_version="v$(jq -r .version < package.json)" + if [[ "${{ github.ref_name }}" != "$pkg_version" ]]; then + echo "Tag ${{ github.ref_name }} does not match package.json version $pkg_version" >&2 + exit 1 + fi + + - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json + + - name: Use Node.js 19 + uses: actions/setup-node@v3 + with: + node-version: 19 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build web + run: pnpm buildWeb --standalone + + - name: Publish extension + run: | + cd dist/extension-unpacked + + # Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later + EXIT_CODE=0 + + # Chrome + pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$? + + # Firefox + npm i -g web-ext@7.4.0 web-ext-submit@7.4.0 + web-ext-submit || EXIT_CODE=$? + + exit $EXIT_CODE + env: + # Chrome + EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }} + CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} + REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} + + # Firefox + WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }} + WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }} + diff --git a/.gitignore b/.gitignore index 7bd751cb9..135673a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules vencord_installer .idea +.DS_Store yarn.lock package-lock.json diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 000000000..6449c3f29 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "stylelint-config-standard", + "rules": { + "indentation": 4 + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8922d157e..f16f1e273 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,11 +1,11 @@ { "recommendations": [ - "EditorConfig.EditorConfig", - "pmneo.tsimporter", "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "EditorConfig.EditorConfig", + "ExodiusStudios.comment-anchors", "formulahendry.auto-rename-tag", "GregorBiswanger.json2ts", - "eamodio.gitlens", - "kamikillerto.vscode-colorize" + "stylelint.vscode-stylelint" ] } diff --git a/README.md b/README.md index 10c73a0e6..048709fd7 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,30 @@ # Vencord -A Discord client mod that does things differently +The cutest Discord client mod ## Features -- Super easy to install, no git or node or anything else required -- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033) - - Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, custom slash commands, ShowHiddenChannels -- Browser Support: Run Vencord in your Browser via extension or UserScript +- Super easy to install (one click installer) +- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033) + - Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB +- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript - Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes) - Works in all Electron versions (Confirmed working on versions 13-23) +- Maintained very actively, broken plugins are usually fixed within 12 hours ## Installing / Uninstalling -If you're just a normal user, use [our simple gui installer!](https://github.com/Vendicated/VencordInstaller#usage) - -If you're a power user who wants to contribute and make plugins or just want to build from source and install manually, read [Megu's Installation Guide!](docs/1_INSTALLING.md) +[![Download and run the Installer ](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#usage) ## Installing on Browser -[![Get the Firefox extension](https://blog.mozilla.org/addons/files/2015/11/get-the-addon-small.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) +[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb) -Or install the browser extension for -- [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip) -- [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS, shiki and other plugins making use of external resources will not work with the UserScript. +Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript. +## Building from Source -You may also build them from source, to do that do the same steps as in the manual regular install method, -except run `pnpm buildWeb` instead of `pnpm build`, and your outputs will be in the dist folder - -```sh -pnpm buildWeb -``` - -You will find the built extension at dist/extension.zip. Now just install this extension in your Browser - -## Installing Plugins - -> **Note** -> You can only use 3rd party plugins in the manual Vencord install for now. - -Vencord comes with a bunch of plugins out of the box! - -However, if you want to install your own ones, create a `userplugins` folder in the `src` directory and create or clone your plugins in there. -Don't forget to rebuild! - -Want to learn how to create your own plugin, and maybe PR it into Vencord? See the [Contributing](#contributing) section below! +See the docs folder ## Contributing diff --git a/browser/background.js b/browser/background.js deleted file mode 100644 index 5c99dd8f6..000000000 --- a/browser/background.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Linnea Gräf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -function setContentTypeOnStylesheets(details) { - if (details.type === "stylesheet") { - details.responseHeaders = details.responseHeaders.filter(it => it.name.toLowerCase() !== 'content-type'); - details.responseHeaders.push({ name: "Content-Type", value: "text/css" }); - } - return { responseHeaders: details.responseHeaders }; -} - -var cspHeaders = [ - "content-security-policy", - "content-security-policy-report-only", -]; - -function removeCSPHeaders(details) { - return { - responseHeaders: details.responseHeaders.filter(header => - !cspHeaders.includes(header.name.toLowerCase())) - }; -} - - - - -browser.webRequest.onHeadersReceived.addListener( - setContentTypeOnStylesheets, { urls: ["https://raw.githubusercontent.com/*"] }, ["blocking", "responseHeaders"] -); - -browser.webRequest.onHeadersReceived.addListener( - removeCSPHeaders, { urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"] }, ["blocking", "responseHeaders"] -); diff --git a/browser/icon.png b/browser/icon.png new file mode 100644 index 000000000..57349fad2 Binary files /dev/null and b/browser/icon.png differ diff --git a/browser/manifestv3.json b/browser/manifest.json similarity index 73% rename from browser/manifestv3.json rename to browser/manifest.json index d15b80a1b..293888051 100644 --- a/browser/manifestv3.json +++ b/browser/manifest.json @@ -1,10 +1,14 @@ { "manifest_version": 3, + "minimum_chrome_version": "91", + "name": "Vencord Web", - "description": "Yeee", - "version": "1.0.0", + "description": "The cutest Discord mod now in your browser", "author": "Vendicated", "homepage_url": "https://github.com/Vendicated/Vencord", + "icons": { + "128": "icon.png" + }, "host_permissions": [ "*://*.discord.com/*", @@ -36,5 +40,12 @@ "path": "modifyResponseHeaders.json" } ] + }, + + "browser_specific_settings": { + "gecko": { + "id": "vencord-firefox@vendicated.dev", + "strict_min_version": "109.0" + } } } diff --git a/browser/manifestv2.json b/browser/manifestv2.json deleted file mode 100644 index b28b73f8d..000000000 --- a/browser/manifestv2.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "manifest_version": 2, - "name": "Vencord Web", - "description": "The Vencord Client Mod for Discord Web.", - "version": "1.0.0", - "author": "Vendicated", - "homepage_url": "https://github.com/Vendicated/Vencord", - "permissions": [ - "webRequest", - "webRequestBlocking", - "*://*.discord.com/*", - "https://raw.githubusercontent.com/*" - ], - "content_scripts": [ - { - "run_at": "document_start", - "matches": ["*://*.discord.com/*"], - "js": ["content.js"] - } - ], - "web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"], - "background": { - "scripts": ["background.js"] - } -} diff --git a/build.mjs b/build.mjs deleted file mode 100644 index 44c18e94a..000000000 --- a/build.mjs +++ /dev/null @@ -1,3 +0,0 @@ -// FIXME: Delete this soon, for now it is needed so people can update - -import("./scripts/build/build.mjs"); diff --git a/docs/1_INSTALLING.md b/docs/1_INSTALLING.md index f27dfc14f..96f1bed56 100644 --- a/docs/1_INSTALLING.md +++ b/docs/1_INSTALLING.md @@ -1,5 +1,5 @@ > **Warning** -> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead. +> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead. # Installation Guide @@ -183,7 +183,6 @@ In `index.js`: ```js require("C:/Users/<your user>/path/to/vencord/dist/patcher.js"); -require("../app.asar"); ``` And in `package.json`: diff --git a/package.json b/package.json index 51d384db9..d3c73ecd2 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "vencord", "private": "true", - "version": "1.0.1", - "description": "A Discord client mod that does things differently", + "version": "1.0.6", + "description": "The cutest Discord client mod", "keywords": [], "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { @@ -20,33 +20,33 @@ "scripts": { "build": "node scripts/build/build.mjs", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", - "inject": "node scripts/patcher/install.js", + "inject": "node scripts/runInstaller.mjs", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint-styles": "stylelint \"src/**/*.css\"", "lint:fix": "pnpm lint --fix", - "test": "pnpm lint && pnpm build && pnpm testTsc", + "test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testTsc": "tsc --noEmit", - "uninject": "node scripts/patcher/uninstall.js", + "uninject": "node scripts/runInstaller.mjs", "watch": "node scripts/build/build.mjs --watch" }, "dependencies": { + "@vap/core": "0.0.12", + "@vap/shiki": "0.10.3", "fflate": "^0.7.4" }, "devDependencies": { "@types/diff": "^5.0.2", - "@types/lodash": "^4.14.0", - "@types/node": "^18.11.9", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.9", + "@types/lodash": "^4.14.191", + "@types/node": "^18.11.18", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", "@types/yazl": "^2.4.2", - "@typescript-eslint/eslint-plugin": "^5.44.0", - "@typescript-eslint/parser": "^5.44.0", - "@vap/core": "0.0.12", - "@vap/shiki": "0.10.3", - "console-menu": "^0.1.0", + "@typescript-eslint/eslint-plugin": "^5.49.0", + "@typescript-eslint/parser": "^5.49.0", "diff": "^5.1.0", "discord-types": "^1.3.26", - "esbuild": "^0.15.16", + "esbuild": "^0.15.18", "eslint": "^8.28.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-header": "^3.1.1", @@ -55,10 +55,12 @@ "eslint-plugin-unused-imports": "^2.0.0", "highlight.js": "10.6.0", "moment": "^2.29.4", - "puppeteer-core": "^19.3.0", + "puppeteer-core": "^19.6.0", "standalone-electron-types": "^1.0.0", - "type-fest": "^3.3.0", - "typescript": "^4.9.3" + "stylelint": "^14.16.1", + "stylelint-config-standard": "^29.0.0", + "type-fest": "^3.5.3", + "typescript": "^4.9.4" }, "packageManager": "pnpm@7.13.4", "pnpm": { @@ -68,7 +70,8 @@ }, "peerDependencyRules": { "ignoreMissing": [ - "eslint-plugin-import" + "eslint-plugin-import", + "eslint" ] }, "allowedDeprecatedVersions": { @@ -84,5 +87,8 @@ "overwriteDest": true }, "sourceDir": "./dist/extension-v2-unpacked" + }, + "engines": { + "node": ">=18" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02c2ea6c3..ac7aca771 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,19 +10,18 @@ patchedDependencies: specifiers: '@types/diff': ^5.0.2 - '@types/lodash': ^4.14.0 - '@types/node': ^18.11.9 - '@types/react': ^18.0.25 - '@types/react-dom': ^18.0.9 + '@types/lodash': ^4.14.191 + '@types/node': ^18.11.18 + '@types/react': ^18.0.27 + '@types/react-dom': ^18.0.10 '@types/yazl': ^2.4.2 - '@typescript-eslint/eslint-plugin': ^5.44.0 - '@typescript-eslint/parser': ^5.44.0 + '@typescript-eslint/eslint-plugin': ^5.49.0 + '@typescript-eslint/parser': ^5.49.0 '@vap/core': 0.0.12 '@vap/shiki': 0.10.3 - console-menu: ^0.1.0 diff: ^5.1.0 discord-types: ^1.3.26 - esbuild: ^0.15.16 + esbuild: ^0.15.18 eslint: ^8.28.0 eslint-import-resolver-alias: ^1.1.2 eslint-plugin-header: ^3.1.1 @@ -32,46 +31,81 @@ specifiers: fflate: ^0.7.4 highlight.js: 10.6.0 moment: ^2.29.4 - puppeteer-core: ^19.3.0 + puppeteer-core: ^19.6.0 standalone-electron-types: ^1.0.0 - type-fest: ^3.3.0 - typescript: ^4.9.3 + stylelint: ^14.16.1 + stylelint-config-standard: ^29.0.0 + type-fest: ^3.5.3 + typescript: ^4.9.4 dependencies: + '@vap/core': 0.0.12 + '@vap/shiki': 0.10.3 fflate: 0.7.4 devDependencies: '@types/diff': 5.0.2 - '@types/lodash': 4.14.189 - '@types/node': 18.11.9 - '@types/react': 18.0.25 - '@types/react-dom': 18.0.9 + '@types/lodash': 4.14.191 + '@types/node': 18.11.18 + '@types/react': 18.0.27 + '@types/react-dom': 18.0.10 '@types/yazl': 2.4.2 - '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au - '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a - '@vap/core': 0.0.12 - '@vap/shiki': 0.10.3 - console-menu: 0.1.0 + '@typescript-eslint/eslint-plugin': 5.49.0_ffoscbl6fkz64kp3vlggrfqozm + '@typescript-eslint/parser': 5.49.0_wy4udjehnvkneqnogzx5kughki diff: 5.1.0 discord-types: 1.3.26 - esbuild: 0.15.16 + esbuild: 0.15.18 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe eslint-import-resolver-alias: 1.1.2 eslint-plugin-header: 3.1.1_eslint@8.28.0 eslint-plugin-path-alias: 1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0 eslint-plugin-simple-import-sort: 8.0.0_eslint@8.28.0 - eslint-plugin-unused-imports: 2.0.0_5am2datodjm2qi4eijrjrnoz54 + eslint-plugin-unused-imports: 2.0.0_nzrwdcb3mq4ezaurfymehngbla highlight.js: 10.6.0 moment: 2.29.4 - puppeteer-core: 19.3.0 + puppeteer-core: 19.6.0 standalone-electron-types: 1.0.0 - type-fest: 3.3.0 - typescript: 4.9.3 + stylelint: 14.16.1 + stylelint-config-standard: 29.0.0_stylelint@14.16.1 + type-fest: 3.5.3 + typescript: 4.9.4 packages: - /@esbuild/android-arm/0.15.16: - resolution: {integrity: sha512-nyB6CH++2mSgx3GbnrJsZSxzne5K0HMyNIWafDHqYy7IwxFc4fd/CgHVZXr8Eh+Q3KbIAcAe3vGyqIPhGblvMQ==} + /@babel/code-frame/7.18.6: + resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + dev: true + + /@babel/helper-validator-identifier/7.19.1: + resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/highlight/7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@csstools/selector-specificity/2.1.1_wajs5nedgkikc5pcuwett7legi: + resolution: {integrity: sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + postcss-selector-parser: ^6.0.10 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: true + + /@esbuild/android-arm/0.15.18: + resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -79,8 +113,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64/0.15.16: - resolution: {integrity: sha512-SDLfP1uoB0HZ14CdVYgagllgrG7Mdxhkt4jDJOKl/MldKrkQ6vDJMZKl2+5XsEY/Lzz37fjgLQoJBGuAw/x8kQ==} + /@esbuild/linux-loong64/0.15.18: + resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -94,7 +128,7 @@ packages: dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.4.0 + espree: 9.4.1 globals: 13.17.0 ignore: 5.2.0 import-fresh: 3.3.0 @@ -154,22 +188,34 @@ packages: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true - /@types/lodash/4.14.189: - resolution: {integrity: sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA==} + /@types/lodash/4.14.191: + resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: true - /@types/node/18.11.9: - resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} + /@types/minimist/1.2.2: + resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} + dev: true + + /@types/node/18.11.18: + resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} + dev: true + + /@types/normalize-package-data/2.4.1: + resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true + + /@types/parse-json/4.0.0: + resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: true /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} dev: true - /@types/react-dom/18.0.9: - resolution: {integrity: sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==} + /@types/react-dom/18.0.10: + resolution: {integrity: sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==} dependencies: - '@types/react': 18.0.25 + '@types/react': 18.0.27 dev: true /@types/react/17.0.2: @@ -179,8 +225,8 @@ packages: csstype: 3.1.0 dev: true - /@types/react/18.0.25: - resolution: {integrity: sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==} + /@types/react/18.0.27: + resolution: {integrity: sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 @@ -199,98 +245,104 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 18.11.9 + '@types/node': 18.11.18 dev: true optional: true /@types/yazl/2.4.2: resolution: {integrity: sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==} dependencies: - '@types/node': 18.11.9 + '@types/node': 18.11.18 dev: true - /@typescript-eslint/eslint-plugin/5.45.0_czs5uoqkd3podpy6vgtsxfc7au: - resolution: {integrity: sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==} + /@typescript-eslint/eslint-plugin/5.49.0_ffoscbl6fkz64kp3vlggrfqozm: + resolution: {integrity: sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 typescript: '*' peerDependenciesMeta: + eslint: + optional: true typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a - '@typescript-eslint/scope-manager': 5.45.0 - '@typescript-eslint/type-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a - '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/parser': 5.49.0_wy4udjehnvkneqnogzx5kughki + '@typescript-eslint/scope-manager': 5.49.0 + '@typescript-eslint/type-utils': 5.49.0_wy4udjehnvkneqnogzx5kughki + '@typescript-eslint/utils': 5.49.0_wy4udjehnvkneqnogzx5kughki debug: 4.3.4 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe ignore: 5.2.0 natural-compare-lite: 1.4.0 regexpp: 3.2.0 semver: 7.3.7 - tsutils: 3.21.0_typescript@4.9.3 - typescript: 4.9.3 + tsutils: 3.21.0_typescript@4.9.4 + typescript: 4.9.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: - resolution: {integrity: sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==} + /@typescript-eslint/parser/5.49.0_wy4udjehnvkneqnogzx5kughki: + resolution: {integrity: sha512-veDlZN9mUhGqU31Qiv2qEp+XrJj5fgZpJ8PW30sHU+j/8/e5ruAhLaVDAeznS7A7i4ucb/s8IozpDtt9NqCkZg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 typescript: '*' peerDependenciesMeta: + eslint: + optional: true typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 5.45.0 - '@typescript-eslint/types': 5.45.0 - '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 + '@typescript-eslint/scope-manager': 5.49.0 + '@typescript-eslint/types': 5.49.0 + '@typescript-eslint/typescript-estree': 5.49.0_typescript@4.9.4 debug: 4.3.4 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe - typescript: 4.9.3 + typescript: 4.9.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager/5.45.0: - resolution: {integrity: sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==} + /@typescript-eslint/scope-manager/5.49.0: + resolution: {integrity: sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.45.0 - '@typescript-eslint/visitor-keys': 5.45.0 + '@typescript-eslint/types': 5.49.0 + '@typescript-eslint/visitor-keys': 5.49.0 dev: true - /@typescript-eslint/type-utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: - resolution: {integrity: sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==} + /@typescript-eslint/type-utils/5.49.0_wy4udjehnvkneqnogzx5kughki: + resolution: {integrity: sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' typescript: '*' peerDependenciesMeta: + eslint: + optional: true typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 - '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/typescript-estree': 5.49.0_typescript@4.9.4 + '@typescript-eslint/utils': 5.49.0_wy4udjehnvkneqnogzx5kughki debug: 4.3.4 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe - tsutils: 3.21.0_typescript@4.9.3 - typescript: 4.9.3 + tsutils: 3.21.0_typescript@4.9.4 + typescript: 4.9.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types/5.45.0: - resolution: {integrity: sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==} + /@typescript-eslint/types/5.49.0: + resolution: {integrity: sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree/5.45.0_typescript@4.9.3: - resolution: {integrity: sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==} + /@typescript-eslint/typescript-estree/5.49.0_typescript@4.9.4: + resolution: {integrity: sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -298,43 +350,46 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 5.45.0 - '@typescript-eslint/visitor-keys': 5.45.0 + '@typescript-eslint/types': 5.49.0 + '@typescript-eslint/visitor-keys': 5.49.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.7 - tsutils: 3.21.0_typescript@4.9.3 - typescript: 4.9.3 + semver: 7.3.8 + tsutils: 3.21.0_typescript@4.9.4 + typescript: 4.9.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: - resolution: {integrity: sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==} + /@typescript-eslint/utils/5.49.0_wy4udjehnvkneqnogzx5kughki: + resolution: {integrity: sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + eslint: + optional: true dependencies: '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 - '@typescript-eslint/scope-manager': 5.45.0 - '@typescript-eslint/types': 5.45.0 - '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 + '@typescript-eslint/scope-manager': 5.49.0 + '@typescript-eslint/types': 5.49.0 + '@typescript-eslint/typescript-estree': 5.49.0_typescript@4.9.4 eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe eslint-scope: 5.1.1 eslint-utils: 3.0.0_eslint@8.28.0 - semver: 7.3.7 + semver: 7.3.8 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys/5.45.0: - resolution: {integrity: sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==} + /@typescript-eslint/visitor-keys/5.49.0: + resolution: {integrity: sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.45.0 + '@typescript-eslint/types': 5.49.0 eslint-visitor-keys: 3.3.0 dev: true @@ -342,7 +397,7 @@ packages: resolution: {integrity: sha512-3csHpkE1zUSRTZwl7xIf2uXg1cD4IhhtUm0F6K/dWydc95R5Nj+krB4OTNATuqkewIv/ViCbwjPfkafAgvZQSg==} dependencies: eventemitter3: 4.0.7 - dev: true + dev: false /@vap/shiki/0.10.3: resolution: {integrity: sha512-tZPHZxDKEBlorQ2BaprytGfkbo5yKBvdxdAF144p94HCTpjO3ScJk/f319wi7GtV1NE4DV8HBQo/0XpldixWQA==} @@ -350,7 +405,7 @@ packages: jsonc-parser: 3.2.0 vscode-oniguruma: 1.7.0 vscode-textmate: 5.2.0 - dev: true + dev: false /acorn-jsx/5.3.2_acorn@8.8.0: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -384,11 +439,27 @@ packages: uri-js: 4.4.1 dev: true + /ajv/8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + /ansi-regex/5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} dev: true + /ansi-styles/3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + /ansi-styles/4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -420,11 +491,21 @@ packages: engines: {node: '>=0.10.0'} dev: true + /arrify/1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + /assign-symbols/1.0.0: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} dev: true + /astral-regex/2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + /atob/2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} @@ -435,6 +516,10 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /balanced-match/2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + dev: true + /base/0.11.2: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} engines: {node: '>=0.10.0'} @@ -505,6 +590,29 @@ packages: engines: {node: '>=6'} dev: true + /camelcase-keys/6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: true + + /camelcase/5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /chalk/2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + /chalk/4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -535,6 +643,12 @@ packages: object-visit: 1.0.1 dev: true + /color-convert/1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + /color-convert/2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -542,10 +656,18 @@ packages: color-name: 1.1.4 dev: true + /color-name/1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /colord/2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + dev: true + /component-emitter/1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} dev: true @@ -554,17 +676,22 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true - /console-menu/0.1.0: - resolution: {integrity: sha512-gOGvuhugXvHggnodbEop0Wzh05eondeCdpPZqcwlzJc7KoPrdsHUM8TZug1lN2jN7Qm3XrZcoP5dZDaO2CaYSw==} - dependencies: - keypress: 0.2.1 - dev: true - /copy-descriptor/0.1.1: resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} engines: {node: '>=0.10.0'} dev: true + /cosmiconfig/7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.0 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: true + /cross-fetch/3.1.5: resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} dependencies: @@ -582,6 +709,17 @@ packages: which: 2.0.2 dev: true + /css-functions-list/3.1.0: + resolution: {integrity: sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==} + engines: {node: '>=12.22'} + dev: true + + /cssesc/3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + /csstype/3.1.0: resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} dev: true @@ -609,6 +747,19 @@ packages: ms: 2.1.2 dev: true + /decamelize-keys/1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize/1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + /decode-uri-component/0.2.0: resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} engines: {node: '>=0.10'} @@ -640,8 +791,8 @@ packages: isobject: 3.0.1 dev: true - /devtools-protocol/0.0.1056733: - resolution: {integrity: sha512-CmTu6SQx2g3TbZzDCAV58+LTxVdKplS7xip0g5oDXpZ+isr0rv5dDP8ToyVRywzPHkCCPKgKgScEcwz4uPWDIA==} + /devtools-protocol/0.0.1082910: + resolution: {integrity: sha512-RqoZ2GmqaNxyx+99L/RemY5CkwG9D0WEfOKxekwCRXOGrDCep62ngezEJUVMq6rISYQ+085fJnWDQqGHlxVNww==} dev: true /diff/5.1.0: @@ -670,14 +821,24 @@ packages: esutils: 2.0.3 dev: true + /emoji-regex/8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + /end-of-stream/1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: once: 1.4.0 dev: true - /esbuild-android-64/0.15.16: - resolution: {integrity: sha512-Vwkv/sT0zMSgPSVO3Jlt1pUbnZuOgtOQJkJkyyJFAlLe7BiT8e9ESzo0zQSx4c3wW4T6kGChmKDPMbWTgtliQA==} + /error-ex/1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /esbuild-android-64/0.15.18: + resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -685,8 +846,8 @@ packages: dev: true optional: true - /esbuild-android-arm64/0.15.16: - resolution: {integrity: sha512-lqfKuofMExL5niNV3gnhMUYacSXfsvzTa/58sDlBET/hCOG99Zmeh+lz6kvdgvGOsImeo6J9SW21rFCogNPLxg==} + /esbuild-android-arm64/0.15.18: + resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -694,8 +855,8 @@ packages: dev: true optional: true - /esbuild-darwin-64/0.15.16: - resolution: {integrity: sha512-wo2VWk/n/9V2TmqUZ/KpzRjCEcr00n7yahEdmtzlrfQ3lfMCf3Wa+0sqHAbjk3C6CKkR3WKK/whkMq5Gj4Da9g==} + /esbuild-darwin-64/0.15.18: + resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -703,8 +864,8 @@ packages: dev: true optional: true - /esbuild-darwin-arm64/0.15.16: - resolution: {integrity: sha512-fMXaUr5ou0M4WnewBKsspMtX++C1yIa3nJ5R2LSbLCfJT3uFdcRoU/NZjoM4kOMKyOD9Sa/2vlgN8G07K3SJnw==} + /esbuild-darwin-arm64/0.15.18: + resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -712,8 +873,8 @@ packages: dev: true optional: true - /esbuild-freebsd-64/0.15.16: - resolution: {integrity: sha512-UzIc0xlRx5x9kRuMr+E3+hlSOxa/aRqfuMfiYBXu2jJ8Mzej4lGL7+o6F5hzhLqWfWm1GWHNakIdlqg1ayaTNQ==} + /esbuild-freebsd-64/0.15.18: + resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -721,8 +882,8 @@ packages: dev: true optional: true - /esbuild-freebsd-arm64/0.15.16: - resolution: {integrity: sha512-8xyiYuGc0DLZphFQIiYaLHlfoP+hAN9RHbE+Ibh8EUcDNHAqbQgUrQg7pE7Bo00rXmQ5Ap6KFgcR0b4ALZls1g==} + /esbuild-freebsd-arm64/0.15.18: + resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -730,8 +891,8 @@ packages: dev: true optional: true - /esbuild-linux-32/0.15.16: - resolution: {integrity: sha512-iGijUTV+0kIMyUVoynK0v+32Oi8yyp0xwMzX69GX+5+AniNy/C/AL1MjFTsozRp/3xQPl7jVux/PLe2ds10/2w==} + /esbuild-linux-32/0.15.18: + resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -739,8 +900,8 @@ packages: dev: true optional: true - /esbuild-linux-64/0.15.16: - resolution: {integrity: sha512-tuSOjXdLw7VzaUj89fIdAaQT7zFGbKBcz4YxbWrOiXkwscYgE7HtTxUavreBbnRkGxKwr9iT/gmeJWNm4djy/g==} + /esbuild-linux-64/0.15.18: + resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -748,8 +909,8 @@ packages: dev: true optional: true - /esbuild-linux-arm/0.15.16: - resolution: {integrity: sha512-XKcrxCEXDTOuoRj5l12tJnkvuxXBMKwEC5j0JISw3ziLf0j4zIwXbKbTmUrKFWbo6ZgvNpa7Y5dnbsjVvH39bQ==} + /esbuild-linux-arm/0.15.18: + resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -757,8 +918,8 @@ packages: dev: true optional: true - /esbuild-linux-arm64/0.15.16: - resolution: {integrity: sha512-mPYksnfHnemNrvjrDhZyixL/AfbJN0Xn9S34ZOHYdh6/jJcNd8iTsv3JwJoEvTJqjMggjMhGUPJAdjnFBHoH8A==} + /esbuild-linux-arm64/0.15.18: + resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -766,8 +927,8 @@ packages: dev: true optional: true - /esbuild-linux-mips64le/0.15.16: - resolution: {integrity: sha512-kSJO2PXaxfm0pWY39+YX+QtpFqyyrcp0ZeI8QPTrcFVQoWEPiPVtOfTZeS3ZKedfH+Ga38c4DSzmKMQJocQv6A==} + /esbuild-linux-mips64le/0.15.18: + resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -775,8 +936,8 @@ packages: dev: true optional: true - /esbuild-linux-ppc64le/0.15.16: - resolution: {integrity: sha512-NimPikwkBY0yGABw6SlhKrtT35sU4O23xkhlrTT/O6lSxv3Pm5iSc6OYaqVAHWkLdVf31bF4UDVFO+D990WpAA==} + /esbuild-linux-ppc64le/0.15.18: + resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -784,8 +945,8 @@ packages: dev: true optional: true - /esbuild-linux-riscv64/0.15.16: - resolution: {integrity: sha512-ty2YUHZlwFOwp7pR+J87M4CVrXJIf5ZZtU/umpxgVJBXvWjhziSLEQxvl30SYfUPq0nzeWKBGw5i/DieiHeKfw==} + /esbuild-linux-riscv64/0.15.18: + resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -793,8 +954,8 @@ packages: dev: true optional: true - /esbuild-linux-s390x/0.15.16: - resolution: {integrity: sha512-VkZaGssvPDQtx4fvVdZ9czezmyWyzpQhEbSNsHZZN0BHvxRLOYAQ7sjay8nMQwYswP6O2KlZluRMNPYefFRs+w==} + /esbuild-linux-s390x/0.15.18: + resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -802,8 +963,8 @@ packages: dev: true optional: true - /esbuild-netbsd-64/0.15.16: - resolution: {integrity: sha512-ElQ9rhdY51et6MJTWrCPbqOd/YuPowD7Cxx3ee8wlmXQQVW7UvQI6nSprJ9uVFQISqSF5e5EWpwWqXZsECLvXg==} + /esbuild-netbsd-64/0.15.18: + resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -811,8 +972,8 @@ packages: dev: true optional: true - /esbuild-openbsd-64/0.15.16: - resolution: {integrity: sha512-KgxMHyxMCT+NdLQE1zVJEsLSt2QQBAvJfmUGDmgEq8Fvjrf6vSKB00dVHUEDKcJwMID6CdgCpvYNt999tIYhqA==} + /esbuild-openbsd-64/0.15.18: + resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -820,8 +981,8 @@ packages: dev: true optional: true - /esbuild-sunos-64/0.15.16: - resolution: {integrity: sha512-exSAx8Phj7QylXHlMfIyEfNrmqnLxFqLxdQF6MBHPdHAjT7fsKaX6XIJn+aQEFiOcE4X8e7VvdMCJ+WDZxjSRQ==} + /esbuild-sunos-64/0.15.18: + resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -829,8 +990,8 @@ packages: dev: true optional: true - /esbuild-windows-32/0.15.16: - resolution: {integrity: sha512-zQgWpY5pUCSTOwqKQ6/vOCJfRssTvxFuEkpB4f2VUGPBpdddZfdj8hbZuFRdZRPIVHvN7juGcpgCA/XCF37mAQ==} + /esbuild-windows-32/0.15.18: + resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -838,8 +999,8 @@ packages: dev: true optional: true - /esbuild-windows-64/0.15.16: - resolution: {integrity: sha512-HjW1hHRLSncnM3MBCP7iquatHVJq9l0S2xxsHHj4yzf4nm9TU4Z7k4NkeMlD/dHQ4jPlQQhwcMvwbJiOefSuZw==} + /esbuild-windows-64/0.15.18: + resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -847,8 +1008,8 @@ packages: dev: true optional: true - /esbuild-windows-arm64/0.15.16: - resolution: {integrity: sha512-oCcUKrJaMn04Vxy9Ekd8x23O8LoU01+4NOkQ2iBToKgnGj5eo1vU9i27NQZ9qC8NFZgnQQZg5oZWAejmbsppNA==} + /esbuild-windows-arm64/0.15.18: + resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -856,34 +1017,39 @@ packages: dev: true optional: true - /esbuild/0.15.16: - resolution: {integrity: sha512-o6iS9zxdHrrojjlj6pNGC2NAg86ECZqIETswTM5KmJitq+R1YmahhWtMumeQp9lHqJaROGnsBi2RLawGnfo5ZQ==} + /esbuild/0.15.18: + resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.15.16 - '@esbuild/linux-loong64': 0.15.16 - esbuild-android-64: 0.15.16 - esbuild-android-arm64: 0.15.16 - esbuild-darwin-64: 0.15.16 - esbuild-darwin-arm64: 0.15.16 - esbuild-freebsd-64: 0.15.16 - esbuild-freebsd-arm64: 0.15.16 - esbuild-linux-32: 0.15.16 - esbuild-linux-64: 0.15.16 - esbuild-linux-arm: 0.15.16 - esbuild-linux-arm64: 0.15.16 - esbuild-linux-mips64le: 0.15.16 - esbuild-linux-ppc64le: 0.15.16 - esbuild-linux-riscv64: 0.15.16 - esbuild-linux-s390x: 0.15.16 - esbuild-netbsd-64: 0.15.16 - esbuild-openbsd-64: 0.15.16 - esbuild-sunos-64: 0.15.16 - esbuild-windows-32: 0.15.16 - esbuild-windows-64: 0.15.16 - esbuild-windows-arm64: 0.15.16 + '@esbuild/android-arm': 0.15.18 + '@esbuild/linux-loong64': 0.15.18 + esbuild-android-64: 0.15.18 + esbuild-android-arm64: 0.15.18 + esbuild-darwin-64: 0.15.18 + esbuild-darwin-arm64: 0.15.18 + esbuild-freebsd-64: 0.15.18 + esbuild-freebsd-arm64: 0.15.18 + esbuild-linux-32: 0.15.18 + esbuild-linux-64: 0.15.18 + esbuild-linux-arm: 0.15.18 + esbuild-linux-arm64: 0.15.18 + esbuild-linux-mips64le: 0.15.18 + esbuild-linux-ppc64le: 0.15.18 + esbuild-linux-riscv64: 0.15.18 + esbuild-linux-s390x: 0.15.18 + esbuild-netbsd-64: 0.15.18 + esbuild-openbsd-64: 0.15.18 + esbuild-sunos-64: 0.15.18 + esbuild-windows-32: 0.15.18 + esbuild-windows-64: 0.15.18 + esbuild-windows-arm64: 0.15.18 + dev: true + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} dev: true /escape-string-regexp/4.0.0: @@ -896,12 +1062,18 @@ packages: engines: {node: '>= 4'} peerDependencies: eslint-plugin-import: '>=1.4.0' + peerDependenciesMeta: + eslint-plugin-import: + optional: true dev: true /eslint-plugin-header/3.1.1_eslint@8.28.0: resolution: {integrity: sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==} peerDependencies: eslint: '>=7.7.0' + peerDependenciesMeta: + eslint: + optional: true dependencies: eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe dev: true @@ -910,6 +1082,9 @@ packages: resolution: {integrity: sha512-FXus57yC+Zd3sMv46pbloXYwFeNVNHJqlACr9V68FG/IzGFBBokGJpmjDbEjpt8ZCeVSndUubeDWWl2A8sCNVQ==} peerDependencies: eslint: ^7 + peerDependenciesMeta: + eslint: + optional: true dependencies: eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe nanomatch: 1.2.13 @@ -922,11 +1097,14 @@ packages: resolution: {integrity: sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==} peerDependencies: eslint: '>=5.0.0' + peerDependenciesMeta: + eslint: + optional: true dependencies: eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe dev: true - /eslint-plugin-unused-imports/2.0.0_5am2datodjm2qi4eijrjrnoz54: + /eslint-plugin-unused-imports/2.0.0_nzrwdcb3mq4ezaurfymehngbla: resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -935,8 +1113,10 @@ packages: peerDependenciesMeta: '@typescript-eslint/eslint-plugin': optional: true + eslint: + optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au + '@typescript-eslint/eslint-plugin': 5.49.0_ffoscbl6fkz64kp3vlggrfqozm eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe eslint-rule-composer: 0.3.0 dev: true @@ -967,6 +1147,9 @@ packages: engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' + peerDependenciesMeta: + eslint: + optional: true dependencies: eslint: 8.28.0_7wc6icvgtg3uswirb5tpsbjnbe eslint-visitor-keys: 2.1.0 @@ -1040,6 +1223,15 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /espree/9.4.1: + resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.0 + acorn-jsx: 5.3.2_acorn@8.8.0 + eslint-visitor-keys: 3.3.0 + dev: true + /esquery/1.4.0: resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} engines: {node: '>=0.10'} @@ -1071,7 +1263,7 @@ packages: /eventemitter3/4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - dev: true + dev: false /extend-shallow/2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} @@ -1125,6 +1317,11 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fastest-levenshtein/1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + dev: true + /fastq/1.13.0: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} dependencies: @@ -1155,6 +1352,14 @@ packages: to-regex-range: 5.0.1 dev: true + /find-up/4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + /find-up/5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1195,6 +1400,10 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + /get-stream/5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -1232,6 +1441,22 @@ packages: path-is-absolute: 1.0.1 dev: true + /global-modules/2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + dependencies: + global-prefix: 3.0.0 + dev: true + + /global-prefix/3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + dev: true + /globals/13.17.0: resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} engines: {node: '>=8'} @@ -1246,15 +1471,29 @@ packages: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.2.12 - ignore: 5.2.0 + ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 dev: true + /globjoin/0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + dev: true + /grapheme-splitter/1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true + /hard-rejection/2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + + /has-flag/3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + /has-flag/4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1291,10 +1530,33 @@ packages: kind-of: 4.0.0 dev: true + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + /highlight.js/10.6.0: resolution: {integrity: sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ==} dev: true + /hosted-git-info/2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /hosted-git-info/4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /html-tags/3.2.0: + resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==} + engines: {node: '>=8'} + dev: true + /https-proxy-agent/5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -1314,6 +1576,11 @@ packages: engines: {node: '>= 4'} dev: true + /ignore/5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + /import-fresh/3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -1322,11 +1589,21 @@ packages: resolve-from: 4.0.0 dev: true + /import-lazy/4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + dev: true + /imurmurhash/0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} dev: true + /indent-string/4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + /inflight/1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -1338,6 +1615,10 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true + /ini/1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + /is-accessor-descriptor/0.1.6: resolution: {integrity: sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==} engines: {node: '>=0.10.0'} @@ -1352,10 +1633,20 @@ packages: kind-of: 6.0.3 dev: true + /is-arrayish/0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + /is-buffer/1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} dev: true + /is-core-module/2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + dependencies: + has: 1.0.3 + dev: true + /is-data-descriptor/0.1.4: resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} engines: {node: '>=0.10.0'} @@ -1405,6 +1696,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point/3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + /is-glob/4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1429,6 +1725,11 @@ packages: engines: {node: '>=8'} dev: true + /is-plain-obj/1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + /is-plain-object/2.0.4: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} @@ -1436,6 +1737,11 @@ packages: isobject: 3.0.1 dev: true + /is-plain-object/5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: true + /is-windows/1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -1465,6 +1771,10 @@ packages: resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} dev: true + /js-tokens/4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + /js-yaml/4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1472,21 +1782,25 @@ packages: argparse: 2.0.1 dev: true + /json-parse-even-better-errors/2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + /json-schema-traverse/0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true + /json-schema-traverse/1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + /json-stable-stringify-without-jsonify/1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true /jsonc-parser/3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - dev: true - - /keypress/0.2.1: - resolution: {integrity: sha512-HjorDJFNhnM4SicvaUXac0X77NiskggxJdesG72+O5zBKpSqKFCrqmndKVqpu3pFqkla0St6uGk8Ju0sCurrmg==} - dev: true + dev: false /kind-of/3.2.2: resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} @@ -1512,6 +1826,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /known-css-properties/0.26.0: + resolution: {integrity: sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==} + dev: true + /levn/0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1520,6 +1838,17 @@ packages: type-check: 0.4.0 dev: true + /lines-and-columns/1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /locate-path/5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + /locate-path/6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1531,6 +1860,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.truncate/4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1543,6 +1876,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /map-obj/1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj/4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + /map-visit/1.0.0: resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} engines: {node: '>=0.10.0'} @@ -1550,6 +1893,28 @@ packages: object-visit: 1.0.1 dev: true + /mathml-tag-names/2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + dev: true + + /meow/9.0.0: + resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} + engines: {node: '>=10'} + dependencies: + '@types/minimist': 1.2.2 + camelcase-keys: 6.2.2 + decamelize: 1.2.0 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + dev: true + /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1563,12 +1928,26 @@ packages: picomatch: 2.3.1 dev: true + /min-indent/1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 dev: true + /minimist-options/4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + /mixin-deep/1.3.2: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} @@ -1593,6 +1972,12 @@ packages: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true + /nanoid/3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /nanomatch/1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -1632,6 +2017,30 @@ packages: whatwg-url: 5.0.0 dev: true + /normalize-package-data/2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.1 + semver: 5.7.1 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-package-data/3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.11.0 + semver: 7.3.8 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + /object-copy/0.1.0: resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} engines: {node: '>=0.10.0'} @@ -1673,6 +2082,13 @@ packages: word-wrap: 1.2.3 dev: true + /p-limit/2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1680,6 +2096,13 @@ packages: yocto-queue: 0.1.0 dev: true + /p-locate/4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + /p-locate/5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -1687,6 +2110,11 @@ packages: p-limit: 3.1.0 dev: true + /p-try/2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + /parent-module/1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1694,6 +2122,16 @@ packages: callsites: 3.1.0 dev: true + /parse-json/5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.18.6 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + /pascalcase/0.1.1: resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} engines: {node: '>=0.10.0'} @@ -1714,6 +2152,10 @@ packages: engines: {node: '>=8'} dev: true + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + /path-type/4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1723,11 +2165,53 @@ packages: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true + /picocolors/1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} dev: true + /postcss-media-query-parser/0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + dev: true + + /postcss-resolve-nested-selector/0.1.1: + resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} + dev: true + + /postcss-safe-parser/6.0.0_postcss@8.4.21: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + dependencies: + postcss: 8.4.21 + dev: true + + /postcss-selector-parser/6.0.11: + resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser/4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss/8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + /prelude-ls/1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1749,20 +2233,20 @@ packages: engines: {node: '>=6'} dev: true - /puppeteer-core/19.3.0: - resolution: {integrity: sha512-P8VAAOBnBJo/7DKJnj1b0K9kZBF2D8lkdL94CjJ+DZKCp182LQqYemPI9omUSZkh4bgykzXjZhaVR1qtddTTQg==} + /puppeteer-core/19.6.0: + resolution: {integrity: sha512-GvqWdHr9eY/MFR5pXf9o0apnrTmG0hhS7/TtCPfeAvCbaUS1bsLMZWxNGvI/QbviRu4xxi6HrR7VW4x/4esq1Q==} engines: {node: '>=14.1.0'} dependencies: cross-fetch: 3.1.5 debug: 4.3.4 - devtools-protocol: 0.0.1056733 + devtools-protocol: 0.0.1082910 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 proxy-from-env: 1.1.0 rimraf: 3.0.2 tar-fs: 2.1.1 unbzip2-stream: 1.4.3 - ws: 8.10.0 + ws: 8.11.0 transitivePeerDependencies: - bufferutil - encoding @@ -1774,6 +2258,30 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /quick-lru/4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: true + + /read-pkg-up/7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: true + + /read-pkg/5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: true + /readable-stream/3.6.0: resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} engines: {node: '>= 6'} @@ -1783,6 +2291,14 @@ packages: util-deprecate: 1.0.2 dev: true + /redent/3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + /regex-not/1.0.2: resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} engines: {node: '>=0.10.0'} @@ -1796,16 +2312,35 @@ packages: engines: {node: '>=8'} dev: true + /require-from-string/2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} dev: true + /resolve-from/5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + /resolve-url/0.2.1: resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} deprecated: https://github.com/lydell/resolve-url#deprecated dev: true + /resolve/1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + /ret/0.1.15: resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} engines: {node: '>=0.12'} @@ -1839,6 +2374,11 @@ packages: ret: 0.1.15 dev: true + /semver/5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: true + /semver/7.3.7: resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} engines: {node: '>=10'} @@ -1847,6 +2387,14 @@ packages: lru-cache: 6.0.0 dev: true + /semver/7.3.8: + resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + /set-value/2.0.1: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} @@ -1869,11 +2417,24 @@ packages: engines: {node: '>=8'} dev: true + /signal-exit/3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} dev: true + /slice-ansi/4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + /snapdragon/0.8.2: resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} engines: {node: '>=0.10.0'} @@ -1890,6 +2451,11 @@ packages: - supports-color dev: true + /source-map-js/1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + /source-map-resolve/0.5.3: resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} deprecated: See https://github.com/lydell/source-map-resolve#deprecated @@ -1911,6 +2477,28 @@ packages: engines: {node: '>=0.10.0'} dev: true + /spdx-correct/3.1.1: + resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.12 + dev: true + + /spdx-exceptions/2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse/3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.12 + dev: true + + /spdx-license-ids/3.0.12: + resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} + dev: true + /split-string/3.1.0: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} engines: {node: '>=0.10.0'} @@ -1921,7 +2509,7 @@ packages: /standalone-electron-types/1.0.0: resolution: {integrity: sha512-0HOi/tlTz3mjWhsAz4uRbpQcHMZ+ifj1JzWW9nugykOHClBBG77ps8QinrzX1eow4Iw2pnC+RFaSYRgufF4BOg==} dependencies: - '@types/node': 18.11.9 + '@types/node': 18.11.18 dev: true /static-extend/0.1.2: @@ -1932,6 +2520,15 @@ packages: object-copy: 0.1.0 dev: true + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + /string_decoder/1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -1945,11 +2542,93 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-indent/3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} dev: true + /style-search/0.1.0: + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} + dev: true + + /stylelint-config-recommended/9.0.0_stylelint@14.16.1: + resolution: {integrity: sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==} + peerDependencies: + stylelint: ^14.10.0 + dependencies: + stylelint: 14.16.1 + dev: true + + /stylelint-config-standard/29.0.0_stylelint@14.16.1: + resolution: {integrity: sha512-uy8tZLbfq6ZrXy4JKu3W+7lYLgRQBxYTUUB88vPgQ+ZzAxdrvcaSUW9hOMNLYBnwH+9Kkj19M2DHdZ4gKwI7tg==} + peerDependencies: + stylelint: ^14.14.0 + dependencies: + stylelint: 14.16.1 + stylelint-config-recommended: 9.0.0_stylelint@14.16.1 + dev: true + + /stylelint/14.16.1: + resolution: {integrity: sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + dependencies: + '@csstools/selector-specificity': 2.1.1_wajs5nedgkikc5pcuwett7legi + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 7.1.0 + css-functions-list: 3.1.0 + debug: 4.3.4 + fast-glob: 3.2.12 + fastest-levenshtein: 1.0.16 + file-entry-cache: 6.0.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.2.0 + ignore: 5.2.4 + import-lazy: 4.0.0 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.26.0 + mathml-tag-names: 2.1.3 + meow: 9.0.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.1 + postcss-safe-parser: 6.0.0_postcss@8.4.21 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + style-search: 0.1.0 + supports-hyperlinks: 2.3.0 + svg-tags: 1.0.0 + table: 6.8.1 + v8-compile-cache: 2.3.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + /supports-color/7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1957,6 +2636,34 @@ packages: has-flag: 4.0.0 dev: true + /supports-hyperlinks/2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /svg-tags/1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + dev: true + + /table/6.8.1: + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.12.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + /tar-fs/2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} dependencies: @@ -2013,18 +2720,23 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true + /trim-newlines/3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + dev: true + /tslib/1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /tsutils/3.21.0_typescript@4.9.3: + /tsutils/3.21.0_typescript@4.9.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 4.9.3 + typescript: 4.9.4 dev: true /type-check/0.4.0: @@ -2034,18 +2746,33 @@ packages: prelude-ls: 1.2.1 dev: true + /type-fest/0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + dev: true + /type-fest/0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} dev: true - /type-fest/3.3.0: - resolution: {integrity: sha512-gezeeOIZyQLGW5uuCeEnXF1aXmtt2afKspXz3YqoOcZ3l/YMJq1pujvgT+cz/Nw1O/7q/kSav5fihJHsC/AOUg==} + /type-fest/0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: true + + /type-fest/0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /type-fest/3.5.3: + resolution: {integrity: sha512-V2+og4j/rWReWvaFrse3s9g2xvUv/K9Azm/xo6CjIuq7oeGqsoimC7+9/A3tfvNcbQf8RPSVj/HV81fB4DJrjA==} engines: {node: '>=14.16'} dev: true - /typescript/4.9.3: - resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} + /typescript/4.9.4: + resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} engines: {node: '>=4.2.0'} hasBin: true dev: true @@ -2095,13 +2822,24 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /v8-compile-cache/2.3.0: + resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} + dev: true + + /validate-npm-package-license/3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.1.1 + spdx-expression-parse: 3.0.1 + dev: true + /vscode-oniguruma/1.7.0: resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} - dev: true + dev: false /vscode-textmate/5.2.0: resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==} - dev: true + dev: false /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -2114,6 +2852,13 @@ packages: webidl-conversions: 3.0.1 dev: true + /which/1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + /which/2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2131,8 +2876,16 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /ws/8.10.0: - resolution: {integrity: sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==} + /write-file-atomic/4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + + /ws/8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -2148,6 +2901,16 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yaml/1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yargs-parser/20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + /yauzl/2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} dependencies: diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 3ad43b2c9..dd7d32ebc 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -82,10 +82,19 @@ async function buildPluginZip(target, files, shouldZip) { const entries = { "dist/Vencord.js": await readFile("dist/browser.js"), "dist/Vencord.css": await readFile("dist/browser.css"), - ...Object.fromEntries(await Promise.all(files.map(async f => [ - (f.startsWith("manifest") ? "manifest.json" : f), - await readFile(join("browser", f)) - ]))), + ...Object.fromEntries(await Promise.all(files.map(async f => { + let content = await readFile(join("browser", f)); + if (f.startsWith("manifest")) { + const json = JSON.parse(content.toString("utf-8")); + json.version = PackageJSON.version; + content = new TextEncoder().encode(JSON.stringify(json)); + } + + return [ + f.startsWith("manifest") ? "manifest.json" : f, + content + ]; + }))), }; if (shouldZip) { @@ -115,21 +124,22 @@ async function buildPluginZip(target, files, shouldZip) { } } -const cssText = "`" + readFileSync("dist/Vencord.user.css", "utf-8").replaceAll("`", "\\`") + "`"; -const cssRuntime = ` +const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => { + const cssRuntime = ` ;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild( Object.assign(document.createElement("style"), { - textContent: ${cssText}, + textContent: \`${content.replaceAll("`", "\\`")}\`, id: "vencord-css-core" - }), - { once: true } -)); + }) +), { once: true }); `; + return appendFile("dist/Vencord.user.js", cssRuntime); +}); + await Promise.all([ - appendFile("dist/Vencord.user.js", cssRuntime), - buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true), - buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true), - buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false), + appendCssRuntime, + buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true), + buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false), ]); diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 2743c700d..c6a082dc9 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -68,11 +68,12 @@ export const globPlugins = { if (!existsSync(`./src/${dir}`)) continue; const files = await readdir(`./src/${dir}`); for (const file of files) { + if (file.startsWith(".")) continue; if (file === "index.ts") { continue; } const mod = `p${i}`; - code += `import ${mod} from "./${dir}/${file.replace(/.tsx?$/, "")}";\n`; + code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`; plugins += `[${mod}.name]:${mod},\n`; i++; } diff --git a/scripts/checkNodeVersion.js b/scripts/checkNodeVersion.js new file mode 100644 index 000000000..041b958be --- /dev/null +++ b/scripts/checkNodeVersion.js @@ -0,0 +1,20 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +if (Number(process.versions.node.split(".")[0]) < 18) + throw `Your node version (${process.version}) is too old, please update to v18 or higher https://nodejs.org/en/download/`; diff --git a/scripts/patcher/common.js b/scripts/patcher/common.js deleted file mode 100644 index 05523e5aa..000000000 --- a/scripts/patcher/common.js +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -const path = require("path"); -const readline = require("readline"); -const fs = require("fs"); -const menu = require("console-menu"); - -function pathToBranch(dir) { - dir = dir.toLowerCase(); - if (dir.endsWith("development")) { - return "development"; - } - if (dir.endsWith("canary")) { - return "canary"; - } - if (dir.endsWith("ptb")) { - return "ptb"; - } - return "stable"; -} - -const BRANCH_NAMES = [ - "Discord", - "DiscordPTB", - "DiscordCanary", - "DiscordDevelopment", - "discord", - "discordptb", - "discordcanary", - "discorddevelopment", - "discord-ptb", - "discord-canary", - "discord-development", - // Flatpak - "com.discordapp.Discord", - "com.discordapp.DiscordPTB", - "com.discordapp.DiscordCanary", - "com.discordapp.DiscordDevelopment", -]; - -const MACOS_DISCORD_DIRS = [ - "Discord.app", - "Discord PTB.app", - "Discord Canary.app", - "Discord Development.app", -]; - -if (process.platform === "linux" && process.env.SUDO_USER) { - process.env.HOME = fs - .readFileSync("/etc/passwd", "utf-8") - .match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0] - .split(":")[5]; -} - -const LINUX_DISCORD_DIRS = [ - "/usr/share", - "/usr/lib64", - "/opt", - `${process.env.HOME}/.local/share`, - `${process.env.HOME}/.dvm`, - "/var/lib/flatpak/app", - `${process.env.HOME}/.local/share/flatpak/app`, -]; - -const FLATPAK_NAME_MAPPING = { - DiscordCanary: "discord-canary", - DiscordPTB: "discord-ptb", - DiscordDevelopment: "discord-development", - Discord: "discord", -}; - -const ENTRYPOINT = path - .join(process.cwd(), "dist", "patcher.js") - .replace(/\\/g, "/"); - -function question(question) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: false, - }); - - return new Promise(resolve => { - rl.question(question, answer => { - rl.close(); - resolve(answer); - }); - }); -} - -async function getMenuItem(installations) { - const menuItems = installations.map(info => ({ - title: info.patched ? "[MODIFIED] " + info.location : info.location, - info, - })); - - const result = await menu( - [ - ...menuItems, - { title: "Specify custom path", info: "custom" }, - { title: "Exit without patching", exit: true } - ], - { - header: "Select a Discord installation to patch:", - border: true, - helpMessage: - "Use the up/down arrow keys to select an option. " + - "Press ENTER to confirm.", - } - ); - - if (!result || !result.info || result.exit) { - console.log("No installation selected."); - process.exit(0); - } - - if (result.info === "custom") { - const customPath = await question("Please enter the path: "); - if (!customPath || !fs.existsSync(customPath)) { - console.log("No such Path or not specifed."); - process.exit(); - } - - const resourceDir = path.join(customPath, "resources"); - if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { - console.log("Unsupported Install. resources/app.asar not found"); - process.exit(); - } - - const appDir = path.join(resourceDir, "app"); - result.info = { - branch: "unknown", - patched: fs.existsSync(appDir), - location: customPath, - versions: [{ - path: appDir, - name: null - }], - arch: process.platform === "linux" ? "linux" : "win32", - isFlatpak: false, - }; - } - - if (result.info.patched) { - const answer = await question( - "This installation has already been modified. Overwrite? [Y/n]: " - ); - - if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) { - console.log("Not patching."); - process.exit(0); - } - } - - return result.info; -} - -function getWindowsDirs() { - const dirs = []; - for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) { - if (!BRANCH_NAMES.includes(dir)) continue; - - const location = path.join(process.env.LOCALAPPDATA, dir); - if (!fs.statSync(location).isDirectory()) continue; - - const appDirs = fs - .readdirSync(location, { withFileTypes: true }) - .filter(file => file.isDirectory()) - .filter(file => file.name.startsWith("app-")) - .map(file => path.join(location, file.name)); - - const versions = []; - let patched = false; - - for (const fqAppDir of appDirs) { - const resourceDir = path.join(fqAppDir, "resources"); - if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { - continue; - } - const appDir = path.join(resourceDir, "app"); - if (fs.existsSync(appDir)) { - patched = true; - } - versions.push({ - path: appDir, - name: /app-([0-9.]+)/.exec(fqAppDir)[1], - }); - } - - if (appDirs.length) { - dirs.push({ - branch: dir, - patched, - location, - versions, - arch: "win32", - flatpak: false, - }); - } - } - return dirs; -} - -function getDarwinDirs() { - const dirs = []; - for (const dir of fs.readdirSync("/Applications")) { - if (!MACOS_DISCORD_DIRS.includes(dir)) continue; - - const location = path.join("/Applications", dir, "Contents"); - if (!fs.existsSync(location)) continue; - if (!fs.statSync(location).isDirectory()) continue; - - const appDirs = fs - .readdirSync(location, { withFileTypes: true }) - .filter(file => file.isDirectory()) - .filter(file => file.name.startsWith("Resources")) - .map(file => path.join(location, file.name)); - - const versions = []; - let patched = false; - - for (const resourceDir of appDirs) { - if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { - continue; - } - const appDir = path.join(resourceDir, "app"); - if (fs.existsSync(appDir)) { - patched = true; - } - - versions.push({ - path: appDir, - name: null, // MacOS installs have no version number - }); - } - - if (appDirs.length) { - dirs.push({ - branch: dir, - patched, - location, - versions, - arch: "win32", - }); - } - } - return dirs; -} - -function getLinuxDirs() { - const dirs = []; - for (const dir of LINUX_DISCORD_DIRS) { - if (!fs.existsSync(dir)) continue; - for (const branch of fs.readdirSync(dir)) { - if (!BRANCH_NAMES.includes(branch)) continue; - - const location = path.join(dir, branch); - if (!fs.statSync(location).isDirectory()) continue; - - const isFlatpak = location.includes("/flatpak/"); - - let appDirs = []; - - if (isFlatpak) { - const fqDir = path.join(location, "current", "active", "files"); - if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue; - const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1]; - if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) { - continue; - } - const appDir = path.join( - fqDir, - FLATPAK_NAME_MAPPING[branchName] - ); - - if (!fs.existsSync(appDir)) continue; - if (!fs.statSync(appDir).isDirectory()) continue; - - const resourceDir = path.join(appDir, "resources"); - - appDirs.push(resourceDir); - } else { - appDirs = fs - .readdirSync(location, { withFileTypes: true }) - .filter(file => file.isDirectory()) - .filter( - file => - file.name.startsWith("app-") || - file.name === "resources" - ) - .map(file => path.join(location, file.name)); - } - - const versions = []; - let patched = false; - - for (const resourceDir of appDirs) { - if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { - continue; - } - const appDir = path.join(resourceDir, "app"); - if (fs.existsSync(appDir)) { - patched = true; - } - - const version = /app-([0-9.]+)/.exec(resourceDir); - - versions.push({ - path: appDir, - name: version && version.length > 1 ? version[1] : null, - }); - } - - if (appDirs.length) { - dirs.push({ - branch, - patched, - location, - versions, - arch: "linux", - isFlatpak, - }); - } - } - } - return dirs; -} - -module.exports = { - pathToBranch, - BRANCH_NAMES, - MACOS_DISCORD_DIRS, - LINUX_DISCORD_DIRS, - FLATPAK_NAME_MAPPING, - ENTRYPOINT, - question, - getMenuItem, - getWindowsDirs, - getDarwinDirs, - getLinuxDirs, -}; diff --git a/scripts/patcher/install.js b/scripts/patcher/install.js deleted file mode 100755 index 3d744a67f..000000000 --- a/scripts/patcher/install.js +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/node -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -const path = require("path"); -const fs = require("fs"); -const { execSync } = require("child_process"); - -console.log("\nVencord Installer\n"); - -if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) { - console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile"); - process.exit(1); -} - -if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) { - console.log("You need to build the project first. Run:", "pnpm build"); - process.exit(1); -} - -const { - getMenuItem, - getWindowsDirs, - getDarwinDirs, - getLinuxDirs, - ENTRYPOINT, - question, - pathToBranch -} = require("./common"); - -switch (process.platform) { - case "win32": - install(getWindowsDirs()); - break; - case "darwin": - install(getDarwinDirs()); - break; - case "linux": - install(getLinuxDirs()); - break; - default: - console.log("Unknown OS"); - break; -} - -async function install(installations) { - const selected = await getMenuItem(installations); - - // Attempt to give flatpak perms - if (selected.isFlatpak) { - try { - const cwd = process.cwd(); - const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`; - const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`; - const cmd = selected.location.startsWith("/home") - ? userCmd - : globalCmd; - execSync(cmd); - console.log("Gave write perms to Discord Flatpak."); - } catch (e) { - console.log("Failed to give write perms to Discord Flatpak."); - console.log( - "Try running this script as an administrator:", - "sudo pnpm inject" - ); - process.exit(1); - } - - const answer = await question( - `Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` + - "This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" + - "Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" + - "[y/N]: " - ); - - if (["y", "yes", "yeah"].includes(answer.toLowerCase())) { - try { - const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`; - const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`; - const cmd = selected.location.startsWith("/home") - ? userCmd - : globalCmd; - execSync(cmd); - console.log("Sucessfully gave talk permission"); - } catch (err) { - console.error("Failed to give talk permission\n", err); - } - } else { - console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`); - } - } - - const useNewMethod = pathToBranch(selected.branch) !== "stable"; - - for (const version of selected.versions) { - - const dir = useNewMethod ? path.join(version.path, "..") : version.path; - - // Check if we have write perms to the install directory... - try { - fs.accessSync(selected.location, fs.constants.W_OK); - } catch (e) { - console.error("No write access to", selected.location); - console.error( - "Make sure Discord isn't running. If that doesn't work,", - "try running this script as an administrator:", - "sudo pnpm inject" - ); - process.exit(1); - } - if (useNewMethod) { - const appAsar = path.join(dir, "app.asar"); - const _appAsar = path.join(dir, "_app.asar"); - - if (fs.existsSync(_appAsar) && fs.existsSync(appAsar)) { - console.log("This copy of Discord already seems to be patched..."); - console.log("Try running `pnpm uninject` first."); - process.exit(1); - } - - try { - fs.renameSync(appAsar, _appAsar); - } catch (err) { - if (err.code === "EBUSY") { - console.error(selected.branch, "is still running. Make sure you fully close it before running this script."); - process.exit(1); - } - console.error("Failed to rename app.asar to _app.asar"); - throw err; - } - - try { - fs.mkdirSync(appAsar); - } catch (err) { - if (err.code === "EBUSY") { - console.error(selected.branch, "is still running. Make sure you fully close it before running this script."); - process.exit(1); - } - console.error("Failed to create app.asar folder"); - throw err; - } - - fs.writeFileSync( - path.join(appAsar, "index.js"), - `require("${ENTRYPOINT}");` - ); - fs.writeFileSync( - path.join(appAsar, "package.json"), - JSON.stringify({ - name: "discord", - main: "index.js", - }) - ); - - const requiredFiles = ["index.js", "package.json"]; - - if (requiredFiles.every(f => fs.existsSync(path.join(appAsar, f)))) { - console.log( - "Successfully patched", - version.name - ? `${selected.branch} ${version.name}` - : selected.branch - ); - } else { - console.log("Failed to patch", dir); - console.log("Files in directory:", fs.readdirSync(appAsar)); - } - - return; - } - if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) { - fs.rmSync(dir, { recursive: true }); - } - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync( - path.join(dir, "index.js"), - `require("${ENTRYPOINT}");` - ); - fs.writeFileSync( - path.join(dir, "package.json"), - JSON.stringify({ - name: "discord", - main: "index.js", - }) - ); - - const requiredFiles = ["index.js", "package.json"]; - - if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) { - console.log( - "Successfully patched", - version.name - ? `${selected.branch} ${version.name}` - : selected.branch - ); - } else { - console.log("Failed to patch", dir); - console.log("Files in directory:", fs.readdirSync(dir)); - } - } -} diff --git a/scripts/patcher/uninstall.js b/scripts/patcher/uninstall.js deleted file mode 100755 index ded6cf9c5..000000000 --- a/scripts/patcher/uninstall.js +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/node -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -const path = require("path"); -const fs = require("fs"); - -console.log("\nVencord Uninstaller\n"); - -if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) { - console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile"); - process.exit(1); -} - -const { - getMenuItem, - getWindowsDirs, - getDarwinDirs, - getLinuxDirs, - pathToBranch, -} = require("./common"); - -switch (process.platform) { - case "win32": - uninstall(getWindowsDirs()); - break; - case "darwin": - uninstall(getDarwinDirs()); - break; - case "linux": - uninstall(getLinuxDirs()); - break; - default: - console.log("Unknown OS"); - break; -} - -async function uninstall(installations) { - const selected = await getMenuItem(installations); - - const useNewMethod = pathToBranch(selected.branch) !== "stable"; - - for (const version of selected.versions) { - const dir = useNewMethod ? path.join(version.path, "..") : version.path; - - // Check if we have write perms to the install directory... - try { - fs.accessSync(selected.location, fs.constants.W_OK); - } catch (e) { - console.error("No write access to", selected.location); - console.error( - "Make sure Discord isn't running. If that doesn't work,", - "try running this script as an administrator:", - "sudo pnpm uninject" - ); - process.exit(1); - } - if (useNewMethod) { - if (!fs.existsSync(path.join(dir, "_app.asar"))) { - console.error( - "Original app.asar (_app.asar) doesn't exist.", - "Is your Discord installation corrupt? Try reinstalling Discord." - ); - process.exit(1); - } - if (fs.existsSync(path.join(dir, "app.asar"))) { - try { - fs.rmSync(path.join(dir, "app.asar"), { force: true, recursive: true }); - } catch (err) { - console.error("Failed to delete app.asar folder"); - throw err; - } - } - try { - fs.renameSync( - path.join(dir, "_app.asar"), - path.join(dir, "app.asar") - ); - } catch (err) { - console.error("Failed to rename _app.asar to app.asar"); - throw err; - } - console.log( - "Successfully unpatched", - version.name - ? `${selected.branch} ${version.name}` - : selected.branch - ); - return; - } - if (fs.existsSync(dir)) { - fs.rmSync(dir, { recursive: true }); - } - console.log( - "Successfully unpatched", - version.name - ? `${selected.branch} ${version.name}` - : selected.branch - ); - } -} diff --git a/scripts/runInstaller.mjs b/scripts/runInstaller.mjs new file mode 100644 index 000000000..b35039f8a --- /dev/null +++ b/scripts/runInstaller.mjs @@ -0,0 +1,128 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import "./checkNodeVersion.js"; + +import { execFileSync, execSync } from "child_process"; +import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { dirname, join } from "path"; +import { Readable } from "stream"; +import { finished } from "stream/promises"; +import { fileURLToPath } from "url"; + +const BASE_URL = "https://github.com/Vencord/Installer/releases/latest/download/"; +const INSTALLER_PATH_DARWIN = "VencordInstaller.app/Contents/MacOS/VencordInstaller"; + +const BASE_DIR = join(dirname(fileURLToPath(import.meta.url)), ".."); +const FILE_DIR = join(BASE_DIR, "dist", "Installer"); +const ETAG_FILE = join(FILE_DIR, "etag.txt"); + +function getFilename() { + switch (process.platform) { + case "win32": + return "VencordInstaller.exe"; + case "darwin": + return "VencordInstaller.MacOS.zip"; + case "linux": + return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11"); + default: + throw new Error("Unsupported platform: " + process.platform); + } +} + +async function ensureBinary() { + const filename = getFilename(); + console.log("Downloading " + filename); + + mkdirSync(FILE_DIR, { recursive: true }); + + const downloadName = join(FILE_DIR, filename); + const outputFile = process.platform === "darwin" + ? join(FILE_DIR, "VencordInstaller") + : downloadName; + + const etag = existsSync(outputFile) && existsSync(ETAG_FILE) + ? readFileSync(ETAG_FILE, "utf-8") + : null; + + const res = await fetch(BASE_URL + filename, { + headers: { + "User-Agent": "Vencord (https://github.com/Vendicated/Vencord)", + "If-None-Match": etag + } + }); + + if (res.status === 304) { + console.log("Up to date, not redownloading!"); + return outputFile; + } + if (!res.ok) + throw new Error(`Failed to download installer: ${res.status} ${res.statusText}`); + + writeFileSync(ETAG_FILE, res.headers.get("etag")); + + if (process.platform === "darwin") { + console.log("Unzipping..."); + const zip = new Uint8Array(await res.arrayBuffer()); + + const ff = await import("fflate"); + const bytes = ff.unzipSync(zip, { + filter: f => f.name === INSTALLER_PATH_DARWIN + })[INSTALLER_PATH_DARWIN]; + + writeFileSync(outputFile, bytes, { mode: 0o755 }); + + console.log("Overriding security policy for installer binary (this is required to run it)"); + console.log("xattr might error, that's okay"); + + const logAndRun = cmd => { + console.log("Running", cmd); + try { + execSync(cmd); + } catch { } + }; + logAndRun(`sudo spctl --add '${outputFile}' --label "Vencord Installer"`); + logAndRun(`sudo xattr -d com.apple.quarantine '${outputFile}'`); + } else { + // WHY DOES NODE FETCH RETURN A WEB STREAM OH MY GOD + const body = Readable.fromWeb(res.body); + await finished(body.pipe(createWriteStream(outputFile, { + mode: 0o755, + autoClose: true + }))); + } + + console.log("Finished downloading!"); + + return outputFile; +} + + + +const installerBin = await ensureBinary(); + +console.log("Now running Installer..."); + +execFileSync(installerBin, { + stdio: "inherit", + env: { + ...process.env, + VENCORD_USER_DATA_DIR: BASE_DIR, + VENCORD_DEV_INSTALL: "1" + } +}); diff --git a/src/Vencord.ts b/src/Vencord.ts index 48e628fde..ac8579bfe 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -30,9 +30,9 @@ import "./webpack/patchWebpack"; import { popNotice, showNotice } from "./api/Notices"; import { PlainSettings, Settings } from "./api/settings"; import { patches, PMLogger, startAllPlugins } from "./plugins"; -import { checkForUpdates, UpdateLogger } from "./utils/updater"; +import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater"; import { onceReady } from "./webpack"; -import { Router } from "./webpack/common"; +import { SettingsRouter } from "./webpack/common"; export let Components: any; @@ -44,17 +44,37 @@ async function init() { if (!IS_WEB) { try { const isOutdated = await checkForUpdates(); - if (isOutdated && Settings.notifyAboutUpdates) + if (!isOutdated) return; + + if (Settings.autoUpdate) { + await update(); + const needsFullRestart = await rebuild(); + setTimeout(() => { + showNotice( + "Vencord has been updated!", + "Restart", + () => { + if (needsFullRestart) + window.DiscordNative.app.relaunch(); + else + location.reload(); + } + ); + }, 10_000); + return; + } + + if (Settings.notifyAboutUpdates) setTimeout(() => { showNotice( "A Vencord update is available!", "View Update", () => { popNotice(); - Router.open("VencordUpdater"); + SettingsRouter.open("VencordUpdater"); } ); - }, 10000); + }, 10_000); } catch (err) { UpdateLogger.error("Failed to check for updates", err); } diff --git a/src/api/Badges.ts b/src/api/Badges.ts index 3607f37eb..d4aabaf21 100644 --- a/src/api/Badges.ts +++ b/src/api/Badges.ts @@ -16,6 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import ErrorBoundary from "@components/ErrorBoundary"; import { User } from "discord-types/general"; import { ComponentType, HTMLProps } from "react"; @@ -52,6 +53,7 @@ const Badges = new Set<ProfileBadge>(); * @param badge The badge to register */ export function addBadge(badge: ProfileBadge) { + badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true }); Badges.add(badge); } diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx new file mode 100644 index 000000000..65d4c433f --- /dev/null +++ b/src/api/Notifications/NotificationComponent.tsx @@ -0,0 +1,92 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import "./styles.css"; + +import { useSettings } from "@api/settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common"; + +import { NotificationData } from "./Notifications"; + +export default ErrorBoundary.wrap(function NotificationComponent({ + title, + body, + richBody, + color, + icon, + onClick, + onClose, + image +}: NotificationData) { + const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications; + const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused()); + + const [isHover, setIsHover] = useState(false); + const [elapsed, setElapsed] = useState(0); + + const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]); + + useEffect(() => { + if (isHover || !hasFocus || timeout === 0) return void setElapsed(0); + + const intervalId = setInterval(() => { + const elapsed = Date.now() - start; + if (elapsed >= timeout) + onClose!(); + else + setElapsed(elapsed); + }, 10); + + return () => clearInterval(intervalId); + }, [timeout, isHover, hasFocus]); + + const timeoutProgress = elapsed / timeout; + + return ( + <button + className="vc-notification-root" + style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }} + onClick={onClick} + onContextMenu={e => { + e.preventDefault(); + e.stopPropagation(); + onClose!(); + }} + onMouseEnter={() => setIsHover(true)} + onMouseLeave={() => setIsHover(false)} + > + <div className="vc-notification"> + {icon && <img className="vc-notification-icon" src={icon} alt="" />} + <div className="vc-notification-content"> + <Forms.FormTitle tag="h2">{title}</Forms.FormTitle> + <div> + {richBody ?? <p className="vc-notification-p">{body}</p>} + </div> + </div> + </div> + {image && <img className="vc-notification-img" src={image} alt="" />} + {timeout !== 0 && ( + <div + className="vc-notification-progressbar" + style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }} + /> + )} + </button> + ); +}); diff --git a/src/api/Notifications/Notifications.tsx b/src/api/Notifications/Notifications.tsx new file mode 100644 index 000000000..46472ad36 --- /dev/null +++ b/src/api/Notifications/Notifications.tsx @@ -0,0 +1,99 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { Settings } from "@api/settings"; +import { Queue } from "@utils/Queue"; +import { ReactDOM } from "@webpack/common"; +import type { ReactNode } from "react"; +import type { Root } from "react-dom/client"; + +import NotificationComponent from "./NotificationComponent"; + +const NotificationQueue = new Queue(); + +let reactRoot: Root; +let id = 42; + +function getRoot() { + if (!reactRoot) { + const container = document.createElement("div"); + container.id = "vc-notification-container"; + document.body.append(container); + reactRoot = ReactDOM.createRoot(container); + } + return reactRoot; +} + +export interface NotificationData { + title: string; + body: string; + /** + * Same as body but can be a custom component. + * Will be used over body if present. + * Not supported on desktop notifications, those will fall back to body */ + richBody?: ReactNode; + /** Small icon. This is for things like profile pictures and should be square */ + icon?: string; + /** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */ + image?: string; + onClick?(): void; + onClose?(): void; + color?: string; +} + +function _showNotification(notification: NotificationData, id: number) { + const root = getRoot(); + return new Promise<void>(resolve => { + root.render( + <NotificationComponent key={id} {...notification} onClose={() => { + notification.onClose?.(); + root.render(null); + resolve(); + }} />, + ); + }); +} + +function shouldBeNative() { + const { useNative } = Settings.notifications; + if (useNative === "always") return true; + if (useNative === "not-focused") return !document.hasFocus(); + return false; +} + +export async function requestPermission() { + return ( + Notification.permission === "granted" || + (Notification.permission !== "denied" && (await Notification.requestPermission()) === "granted") + ); +} + +export async function showNotification(data: NotificationData) { + if (shouldBeNative() && await requestPermission()) { + const { title, body, icon, image, onClick = null, onClose = null } = data; + const n = new Notification(title, { + body, + icon, + image + }); + n.onclick = onClick; + n.onclose = onClose; + } else { + NotificationQueue.push(() => _showNotification(data, id++)); + } +} diff --git a/src/api/Notifications/index.ts b/src/api/Notifications/index.ts new file mode 100644 index 000000000..cd14587b6 --- /dev/null +++ b/src/api/Notifications/index.ts @@ -0,0 +1,19 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export * from "./Notifications"; diff --git a/src/api/Notifications/styles.css b/src/api/Notifications/styles.css new file mode 100644 index 000000000..84d8ff777 --- /dev/null +++ b/src/api/Notifications/styles.css @@ -0,0 +1,49 @@ +.vc-notification-root { + /* clear default button styles */ + all: unset; + display: flex; + flex-direction: column; + width: 25vw; + min-height: 10vh; + color: var(--text-normal); + background-color: var(--background-secondary-alt); + position: absolute; + z-index: 2147483647; + right: 1rem; + border-radius: 6px; + overflow: hidden; + cursor: pointer; +} + +.vc-notification { + display: flex; + flex-direction: row; + padding: 1.25rem; + gap: 1.25rem; +} + +.vc-notification-icon { + height: 4rem; + width: 4rem; + border-radius: 6px; +} + +/* Discord adding 3km margin to generic tags */ +.vc-notification h2 { + margin: unset; +} + +.vc-notification-progressbar { + height: 0.25rem; + border-radius: 5px; + margin-top: auto; +} + +.vc-notification-p { + margin: 0.5rem 0 0; + line-height: 140%; +} + +.vc-notification-img { + width: 100%; +} diff --git a/src/api/index.ts b/src/api/index.ts index 0fef99cda..abb509348 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -25,6 +25,7 @@ import * as $MessageDecorations from "./MessageDecorations"; import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessagePopover from "./MessagePopover"; import * as $Notices from "./Notices"; +import * as $Notifications from "./Notifications"; import * as $ServerList from "./ServerList"; import * as $Styles from "./Styles"; @@ -88,3 +89,7 @@ export const MemberListDecorators = $MemberListDecorators; * a */ export const Styles = $Styles; +/** + * An API allowing you to display notifications + */ +export const Notifications = $Notifications; diff --git a/src/api/settings.ts b/src/api/settings.ts index 2617903a5..c7117918a 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -19,7 +19,7 @@ import IpcEvents from "@utils/IpcEvents"; import Logger from "@utils/Logger"; import { mergeDefaults } from "@utils/misc"; -import { OptionType } from "@utils/types"; +import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types"; import { React } from "@webpack/common"; import plugins from "~plugins"; @@ -27,23 +27,43 @@ import plugins from "~plugins"; const logger = new Logger("Settings"); export interface Settings { notifyAboutUpdates: boolean; + autoUpdate: boolean; useQuickCss: boolean; enableReactDevtools: boolean; themeLinks: string[]; + frameless: boolean; + transparent: boolean; + winCtrlQ: boolean; plugins: { [plugin: string]: { enabled: boolean; [setting: string]: any; }; }; + + notifications: { + timeout: number; + position: "top-right" | "bottom-right"; + useNative: "always" | "never" | "not-focused"; + }; } const DefaultSettings: Settings = { notifyAboutUpdates: true, + autoUpdate: false, useQuickCss: true, themeLinks: [], enableReactDevtools: false, - plugins: {} + frameless: false, + transparent: false, + winCtrlQ: false, + plugins: {}, + + notifications: { + timeout: 5000, + position: "bottom-right", + useNative: "not-focused" + } }; try { @@ -144,6 +164,7 @@ export const Settings = makeProxy(settings); * @param paths An optional list of paths to whitelist for rerenders * @returns Settings */ +// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later export function useSettings(paths?: string[]) { const [, forceUpdate] = React.useReducer(() => ({}), {}); @@ -198,3 +219,19 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) { } } } + +export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) { + const definedSettings: DefinedSettings<D> = { + get store() { + if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized"); + return Settings.plugins[definedSettings.pluginName] as any; + }, + use: settings => useSettings( + settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) + ).plugins[definedSettings.pluginName] as any, + def, + checks: checks ?? {}, + pluginName: "", + }; + return definedSettings; +} diff --git a/src/components/PluginSettings/components/BadgeComponent.tsx b/src/components/Badge.tsx similarity index 84% rename from src/components/PluginSettings/components/BadgeComponent.tsx rename to src/components/Badge.tsx index 059376fd5..0ed45ff81 100644 --- a/src/components/PluginSettings/components/BadgeComponent.tsx +++ b/src/components/Badge.tsx @@ -16,15 +16,14 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { BadgeStyle } from "@components/PluginSettings/styles"; - export function Badge({ text, color }): JSX.Element { return ( - <div style={{ + <div className="vc-plugins-badge" style={{ backgroundColor: color, justifySelf: "flex-end", - marginLeft: "auto", - ...BadgeStyle - }}>{text}</div> + marginLeft: "auto" + }}> + {text} + </div> ); } diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 8ebc61bbb..a13640e10 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -103,7 +103,7 @@ const ErrorBoundary = LazyComponent(() => { }; }) as React.ComponentType<React.PropsWithChildren<Props>> & { - wrap<T extends JSX.IntrinsicAttributes = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>; + wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>; }; ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => ( diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 46568505c..43e1d31ab 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -144,6 +144,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti onChange={onChange} onError={onError} pluginSettings={pluginSettings} + definedSettings={plugin.settings} /> ); }); diff --git a/src/components/PluginSettings/components/SettingBooleanComponent.tsx b/src/components/PluginSettings/components/SettingBooleanComponent.tsx index 0aaafa049..c90af1684 100644 --- a/src/components/PluginSettings/components/SettingBooleanComponent.tsx +++ b/src/components/PluginSettings/components/SettingBooleanComponent.tsx @@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common"; import { ISettingElementProps } from "."; -export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) { +export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) { const def = pluginSettings[id] ?? option.default; const [state, setState] = React.useState(def ?? false); @@ -37,7 +37,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange, ]; function handleChange(newValue: boolean): void { - const isValid = (option.isValid && option.isValid(newValue)) ?? true; + const isValid = option.isValid?.call(definedSettings, newValue) ?? true; if (typeof isValid === "string") setError(isValid); else if (!isValid) setError("Invalid input provided."); else { @@ -51,7 +51,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange, <Forms.FormSection> <Forms.FormTitle>{option.description}</Forms.FormTitle> <Select - isDisabled={option.disabled?.() ?? false} + isDisabled={option.disabled?.call(definedSettings) ?? false} options={options} placeholder={option.placeholder ?? "Select an option"} maxVisibleItems={5} diff --git a/src/components/PluginSettings/components/SettingNumericComponent.tsx b/src/components/PluginSettings/components/SettingNumericComponent.tsx index 3457783e9..12e8e3600 100644 --- a/src/components/PluginSettings/components/SettingNumericComponent.tsx +++ b/src/components/PluginSettings/components/SettingNumericComponent.tsx @@ -23,7 +23,7 @@ import { ISettingElementProps } from "."; const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER); -export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) { +export function SettingNumericComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) { function serialize(value: any) { if (option.type === OptionType.BIGINT) return BigInt(value); return Number(value); @@ -37,7 +37,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange, }, [error]); function handleChange(newValue) { - const isValid = (option.isValid && option.isValid(newValue)) ?? true; + const isValid = option.isValid?.call(definedSettings, newValue) ?? true; if (typeof isValid === "string") setError(isValid); else if (!isValid) setError("Invalid input provided."); else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) { @@ -58,7 +58,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange, value={state} onChange={handleChange} placeholder={option.placeholder ?? "Enter a number"} - disabled={option.disabled?.() ?? false} + disabled={option.disabled?.call(definedSettings) ?? false} {...option.componentProps} /> {error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>} diff --git a/src/components/PluginSettings/components/SettingSelectComponent.tsx b/src/components/PluginSettings/components/SettingSelectComponent.tsx index 8a5bee1cd..164a29a4c 100644 --- a/src/components/PluginSettings/components/SettingSelectComponent.tsx +++ b/src/components/PluginSettings/components/SettingSelectComponent.tsx @@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common"; import { ISettingElementProps } from "."; -export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) { +export function SettingSelectComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) { const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value; const [state, setState] = React.useState<any>(def ?? null); @@ -32,7 +32,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr }, [error]); function handleChange(newValue) { - const isValid = (option.isValid && option.isValid(newValue)) ?? true; + const isValid = option.isValid?.call(definedSettings, newValue) ?? true; if (typeof isValid === "string") setError(isValid); else if (!isValid) setError("Invalid input provided."); else { @@ -45,7 +45,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr <Forms.FormSection> <Forms.FormTitle>{option.description}</Forms.FormTitle> <Select - isDisabled={option.disabled?.() ?? false} + isDisabled={option.disabled?.call(definedSettings) ?? false} options={option.options} placeholder={option.placeholder ?? "Select an option"} maxVisibleItems={5} diff --git a/src/components/PluginSettings/components/SettingSliderComponent.tsx b/src/components/PluginSettings/components/SettingSliderComponent.tsx index 2d3169679..3dda19be1 100644 --- a/src/components/PluginSettings/components/SettingSliderComponent.tsx +++ b/src/components/PluginSettings/components/SettingSliderComponent.tsx @@ -29,7 +29,7 @@ export function makeRange(start: number, end: number, step = 1) { return ranges; } -export function SettingSliderComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) { +export function SettingSliderComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) { const def = pluginSettings[id] ?? option.default; const [error, setError] = React.useState<string | null>(null); @@ -39,7 +39,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o }, [error]); function handleChange(newValue: number): void { - const isValid = (option.isValid && option.isValid(newValue)) ?? true; + const isValid = option.isValid?.call(definedSettings, newValue) ?? true; if (typeof isValid === "string") setError(isValid); else if (!isValid) setError("Invalid input provided."); else { @@ -52,7 +52,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o <Forms.FormSection> <Forms.FormTitle>{option.description}</Forms.FormTitle> <Slider - disabled={option.disabled?.() ?? false} + disabled={option.disabled?.call(definedSettings) ?? false} markers={option.markers} minValue={option.markers[0]} maxValue={option.markers[option.markers.length - 1]} diff --git a/src/components/PluginSettings/components/SettingTextComponent.tsx b/src/components/PluginSettings/components/SettingTextComponent.tsx index b92fceccf..599593faf 100644 --- a/src/components/PluginSettings/components/SettingTextComponent.tsx +++ b/src/components/PluginSettings/components/SettingTextComponent.tsx @@ -21,7 +21,7 @@ import { Forms, React, TextInput } from "@webpack/common"; import { ISettingElementProps } from "."; -export function SettingTextComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) { +export function SettingTextComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) { const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null); const [error, setError] = React.useState<string | null>(null); @@ -30,7 +30,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE }, [error]); function handleChange(newValue) { - const isValid = (option.isValid && option.isValid(newValue)) ?? true; + const isValid = option.isValid?.call(definedSettings, newValue) ?? true; if (typeof isValid === "string") setError(isValid); else if (!isValid) setError("Invalid input provided."); else { @@ -47,7 +47,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE value={state} onChange={handleChange} placeholder={option.placeholder ?? "Enter a value"} - disabled={option.disabled?.() ?? false} + disabled={option.disabled?.call(definedSettings) ?? false} {...option.componentProps} /> {error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>} diff --git a/src/components/PluginSettings/components/index.ts b/src/components/PluginSettings/components/index.ts index d44fb386f..d307b4e68 100644 --- a/src/components/PluginSettings/components/index.ts +++ b/src/components/PluginSettings/components/index.ts @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { PluginOptionBase } from "@utils/types"; +import { DefinedSettings, PluginOptionBase } from "@utils/types"; export interface ISettingElementProps<T extends PluginOptionBase> { option: T; @@ -27,9 +27,10 @@ export interface ISettingElementProps<T extends PluginOptionBase> { }; id: string; onError(hasError: boolean): void; + definedSettings?: DefinedSettings; } -export * from "./BadgeComponent"; +export * from "../../Badge"; export * from "./SettingBooleanComponent"; export * from "./SettingCustomComponent"; export * from "./SettingNumericComponent"; diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 981891457..4e64eb87e 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -16,28 +16,32 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import "./styles.css"; + import * as DataStore from "@api/DataStore"; import { showNotice } from "@api/Notices"; -import { Settings, useSettings } from "@api/settings"; +import { useSettings } from "@api/settings"; +import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; -import { ErrorCard } from "@components/ErrorCard"; import { Flex } from "@components/Flex"; import { handleComponentFailed } from "@components/handleComponentFailed"; import { Badge } from "@components/PluginSettings/components"; import PluginModal from "@components/PluginSettings/PluginModal"; -import * as styles from "@components/PluginSettings/styles"; +import { Switch } from "@components/Switch"; import { ChangeList } from "@utils/ChangeList"; import Logger from "@utils/Logger"; import { classes, LazyComponent, useAwaiter } from "@utils/misc"; import { openModalLazy } from "@utils/modal"; import { Plugin } from "@utils/types"; import { findByCode, findByPropsLazy } from "@webpack"; -import { Alerts, Button, Forms, Margins, Parser, React, Select, Switch, Text, TextInput, Toasts, Tooltip } from "@webpack/common"; +import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common"; import Plugins from "~plugins"; import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins"; + +const cl = classNameFactory("vc-plugins-"); const logger = new Logger("PluginSettings", "#a6d189"); const InputStyles = findByPropsLazy("inputDefault", "inputWrapper"); @@ -56,23 +60,27 @@ function showErrorToast(message: string) { }); } -interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> { - plugins: string[]; -} - -function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) { - if (plugins.length === 0) return null; - - const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:"; - const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : "."; - +function ReloadRequiredCard({ required }: { required: boolean; }) { return ( - <ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}> - <span style={{ margin: "auto 0" }}> - {pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix} - </span> - <Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button> - </ErrorCard> + <Card className={cl("info-card", { "restart-card": required })}> + {required ? ( + <> + <Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle> + <Forms.FormText className={cl("dep-text")}> + Restart now to apply new plugins and their settings + </Forms.FormText> + <Button color={Button.Colors.YELLOW} onClick={() => location.reload()}> + Restart + </Button> + </> + ) : ( + <> + <Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle> + <Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText> + <Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText> + </> + )} + </Card> ); } @@ -84,14 +92,9 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> { } function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) { - const settings = useSettings(); - const pluginSettings = settings.plugins[plugin.name]; + const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name]; - const [iconHover, setIconHover] = React.useState(false); - - function isEnabled() { - return pluginSettings?.enabled || plugin.started; - } + const isEnabled = () => settings.enabled ?? false; function openModal() { openModalLazy(async () => { @@ -113,7 +116,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe return; } else if (restartNeeded) { // If any dependencies have patches, don't start the plugin yet. - pluginSettings.enabled = true; + settings.enabled = true; onRestartNeeded(plugin.name); return; } @@ -121,14 +124,14 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe // if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes. if (plugin.patches) { - pluginSettings.enabled = !wasEnabled; + settings.enabled = !wasEnabled; onRestartNeeded(plugin.name); return; } // If the plugin is enabled, but hasn't been started, then we can just toggle it off. if (wasEnabled && !plugin.started) { - pluginSettings.enabled = !wasEnabled; + settings.enabled = !wasEnabled; return; } @@ -141,60 +144,38 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe return; } - pluginSettings.enabled = !wasEnabled; + settings.enabled = !wasEnabled; } return ( - <Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> - <Switch - onChange={toggleEnabled} - disabled={disabled} - value={isEnabled()} - note={<Text variant="text-md/normal" style={{ - height: 40, - overflow: "hidden", - // mfw css is so bad you need whatever this is to get multi line overflow ellipsis to work - textOverflow: "ellipsis", - display: "-webkit-box", // firefox users will cope (it doesn't support it) - WebkitLineClamp: 2, - lineClamp: 2, - WebkitBoxOrient: "vertical", - boxOrient: "vertical" - }}> - {plugin.description} - </Text>} - hideBorder={true} - > - <Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center", gap: "8px" }}> - <Text - variant="text-md/bold" - style={{ - display: "flex", width: "100%", alignItems: "center", flexGrow: "1", gap: "8px" - }} - > - {plugin.name}{(isNew) && <Badge text="NEW" color="#ED4245" />} - </Text> - <button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur"> - {plugin.options - ? <CogWheel - style={{ color: iconHover ? "" : "var(--text-muted)" }} - onMouseEnter={() => setIconHover(true)} - onMouseLeave={() => setIconHover(false)} - /> - : <InfoIcon - width="24" height="24" - style={{ color: iconHover ? "" : "var(--text-muted)" }} - onMouseEnter={() => setIconHover(true)} - onMouseLeave={() => setIconHover(false)} - />} - </button> - </Flex> - </Switch> - </Flex> + <Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> + <div className={cl("card-header")}> + <Text variant="text-md/bold" className={cl("name")}> + {plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />} + </Text> + <button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}> + {plugin.options + ? <CogWheel /> + : <InfoIcon width="24" height="24" />} + </button> + <Switch + checked={isEnabled()} + onChange={toggleEnabled} + disabled={disabled} + /> + </div> + <Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text> + </Flex > ); } -export default ErrorBoundary.wrap(function Settings() { +enum SearchStatus { + ALL, + ENABLED, + DISABLED +} + +export default ErrorBoundary.wrap(function PluginSettings() { const settings = useSettings(); const changes = React.useMemo(() => new ChangeList<string>(), []); @@ -235,21 +216,19 @@ export default ErrorBoundary.wrap(function Settings() { const sortedPlugins = React.useMemo(() => Object.values(Plugins) .sort((a, b) => a.name.localeCompare(b.name)), []); - const [searchValue, setSearchValue] = React.useState({ value: "", status: "all" }); + const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL }); const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query })); - const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status })); + const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status })); const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => { - const showEnabled = searchValue.status === "enabled" || searchValue.status === "all"; - const showDisabled = searchValue.status === "disabled" || searchValue.status === "all"; - const enabled = settings.plugins[plugin.name]?.enabled || plugin.started; + const enabled = settings.plugins[plugin.name]?.enabled; + if (enabled && searchValue.status === SearchStatus.DISABLED) return false; + if (!enabled && searchValue.status === SearchStatus.ENABLED) return false; + if (!searchValue.value.length) return true; return ( - ((showEnabled && enabled) || (showDisabled && !enabled)) && - ( - plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) || - plugin.description.toLowerCase().includes(searchValue.value.toLowerCase()) - ) + plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) || + plugin.description.toLowerCase().includes(searchValue.value.toLowerCase()) ); }; @@ -270,23 +249,69 @@ export default ErrorBoundary.wrap(function Settings() { return window._.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins; })); + type P = JSX.Element | JSX.Element[]; + let plugins: P, requiredPlugins: P; + if (sortedPlugins?.length) { + plugins = []; + requiredPlugins = []; + + for (const p of sortedPlugins) { + if (!pluginFilter(p)) continue; + + const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled); + + if (isRequired) { + const tooltipText = p.required + ? "This plugin is required for Vencord to function." + : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled)); + + requiredPlugins.push( + <Tooltip text={tooltipText} key={p.name}> + {({ onMouseLeave, onMouseEnter }) => ( + <PluginCard + onMouseLeave={onMouseLeave} + onMouseEnter={onMouseEnter} + onRestartNeeded={name => changes.handleChange(name)} + disabled={true} + plugin={p} + /> + )} + </Tooltip> + ); + } else { + plugins.push( + <PluginCard + onRestartNeeded={name => changes.handleChange(name)} + disabled={false} + plugin={p} + isNew={newPlugins?.includes(p.name)} + key={p.name} + /> + ); + } + + } + } else { + plugins = requiredPlugins = <Text variant="text-md/normal">No plugins meet search criteria.</Text>; + } + return ( - <Forms.FormSection> + <Forms.FormSection className={Margins.marginTop16}> + <ReloadRequiredCard required={changes.hasChanges} /> + <Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}> Filters </Forms.FormTitle> - <ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} /> - - <div style={styles.FiltersBar}> - <TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} /> + <div className={cl("filter-controls")}> + <TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} /> <div className={InputStyles.inputWrapper}> <Select className={InputStyles.inputDefault} options={[ - { label: "Show All", value: "all", default: true }, - { label: "Show Enabled", value: "enabled" }, - { label: "Show Disabled", value: "disabled" } + { label: "Show All", value: SearchStatus.ALL, default: true }, + { label: "Show Enabled", value: SearchStatus.ENABLED }, + { label: "Show Disabled", value: SearchStatus.DISABLED } ]} serialize={String} select={onStatusChange} @@ -298,50 +323,17 @@ export default ErrorBoundary.wrap(function Settings() { <Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle> - <div style={styles.PluginsGrid}> - {sortedPlugins?.length ? sortedPlugins - .filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a)) - .map(plugin => { - const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled); - const dependency = enabledDependants?.length; - return <PluginCard - onRestartNeeded={name => changes.add(name)} - disabled={plugin.required || !!dependency} - plugin={plugin} - isNew={newPlugins?.includes(plugin.name)} - key={plugin.name} - />; - }) - : <Text variant="text-md/normal">No plugins meet search criteria.</Text> - } + <div className={cl("grid")}> + {plugins} </div> - <Forms.FormDivider /> + + <Forms.FormDivider className={Margins.marginTop20} /> + <Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}> Required Plugins </Forms.FormTitle> - <div style={styles.PluginsGrid}> - {sortedPlugins?.length ? sortedPlugins - .filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a)) - .map(plugin => { - const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled); - const dependency = enabledDependants?.length; - const tooltipText = plugin.required - ? "This plugin is required for Vencord to function." - : makeDependencyList(dependencyCheck(plugin.name, depMap)); - return <Tooltip text={tooltipText} key={plugin.name}> - {({ onMouseLeave, onMouseEnter }) => ( - <PluginCard - onMouseLeave={onMouseLeave} - onMouseEnter={onMouseEnter} - onRestartNeeded={name => changes.handleChange(name)} - disabled={plugin.required || !!dependency} - plugin={plugin} - /> - )} - </Tooltip>; - }) - : <Text variant="text-md/normal">No plugins meet search criteria.</Text> - } + <div className={cl("grid")}> + {requiredPlugins} </div> </Forms.FormSection > ); @@ -354,11 +346,7 @@ function makeDependencyList(deps: string[]) { return ( <React.Fragment> <Forms.FormText>This plugin is required by:</Forms.FormText> - {deps.map((dep: string) => <Forms.FormText style={{ margin: "0 auto" }}>{dep}</Forms.FormText>)} + {deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)} </React.Fragment> ); } - -function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] { - return depMap[pluginName]?.filter(d => Settings.plugins[d].enabled) || []; -} diff --git a/src/components/PluginSettings/styles.css b/src/components/PluginSettings/styles.css new file mode 100644 index 000000000..a756fa9d5 --- /dev/null +++ b/src/components/PluginSettings/styles.css @@ -0,0 +1,138 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +.vc-plugins-grid { + margin-top: 16px; + display: grid; + grid-gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); +} + +.vc-plugins-card { + background-color: var(--background-secondary-alt); + color: var(--interactive-active); + border-radius: 8px; + display: block; + height: 100%; + padding: 12px; + width: 100%; + transition: 0.1s ease-out; + transition-property: box-shadow, transform, background, opacity; +} + +.vc-plugins-card-disabled { + opacity: 0.6; +} + +.vc-plugins-card:hover { + background-color: var(--background-tertiary); + transform: translateY(-1px); + box-shadow: var(--elevation-high); +} + +.vc-plugins-card-header { + margin-top: auto; + display: flex; + width: 100%; + justify-content: flex-end; + height: 1.5rem; + align-items: center; + gap: 8px; +} + +.vc-plugins-info-button { + height: 24px; + width: 24px; + padding: 0; + background: transparent; + margin-right: 8px; +} + +.vc-plugins-settings-button:hover { + color: var(--interactive-hover); +} + +.vc-plugins-filter-controls { + display: grid; + height: 40px; + gap: 10px; + grid-template-columns: 1fr 150px; +} + +.vc-plugins-badge { + padding: 0 6px; + font-family: var(--font-display); + font-weight: 500; + border-radius: 8px; + height: 16px; + font-size: 12px; + line-height: 16px; + color: var(--white-500); + text-align: center; +} + +.vc-plugins-note { + height: 36px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + /* stylelint-disable-next-line property-no-unknown */ + box-orient: vertical; +} + +.vc-plugins-name { + display: flex; + width: 100%; + align-items: center; + flex-grow: 1; + gap: 8px; + cursor: "default"; +} + +.vc-plugins-dep-name { + margin: 0 auto; +} + +.vc-plugins-info-card { + padding: 1em; + height: 8em; + display: flex; + flex-direction: column; +} + +.vc-plugins-info-card div { + line-height: 32px; +} + +.vc-plugins-restart-card { + padding: 1em; + background: var(--info-warning-background); + border: 1px solid var(--info-warning-foreground); + color: var(--info-warning-text); +} + +.vc-plugins-restart-card button { + margin-top: 0.5em; +} + +.vc-plugins-info-button svg:not(:hover, :focus) { + color: var(--text-muted); +} diff --git a/src/components/PluginSettings/styles.ts b/src/components/PluginSettings/styles.ts deleted file mode 100644 index 5621b7617..000000000 --- a/src/components/PluginSettings/styles.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -export const PluginsGrid: React.CSSProperties = { - marginTop: 16, - display: "grid", - gridGap: 16, - gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))", -}; - -export const PluginsGridItem: React.CSSProperties = { - backgroundColor: "var(--background-modifier-selected)", - color: "var(--interactive-active)", - borderRadius: 3, - cursor: "pointer", - display: "block", - height: "100%", - padding: 10, - width: "100%", -}; - -export const FiltersBar: React.CSSProperties = { - gap: 10, - height: 40, - gridTemplateColumns: "1fr 150px", - display: "grid" -}; - -export const SettingsIcon: React.CSSProperties = { - height: "24px", - width: "24px", - padding: "0", - background: "transparent", - marginRight: 8 -}; - -export const BadgeStyle: React.CSSProperties = { - padding: "0 6px", - fontFamily: "var(--font-display)", - fontWeight: "500", - borderRadius: "8px", - height: "16px", - fontSize: "12px", - lineHeight: "16px", - color: "var(--white-500)", -}; diff --git a/src/components/Switch.css b/src/components/Switch.css new file mode 100644 index 000000000..e6dcf5626 --- /dev/null +++ b/src/components/Switch.css @@ -0,0 +1,3 @@ +.vc-switch-slider { + transition: 100ms transform ease-in-out; +} diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx new file mode 100644 index 000000000..11a3fe02e --- /dev/null +++ b/src/components/Switch.tsx @@ -0,0 +1,76 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import "./Switch.css"; + +import { findByPropsLazy } from "@webpack"; + +interface SwitchProps { + checked: boolean; + onChange: (checked: boolean) => void; + disabled?: boolean; +} + +const SWITCH_ON = "var(--green-360)"; +const SWITCH_OFF = "var(--primary-400)"; +const SwitchClasses = findByPropsLazy("slider", "input", "container"); + +export function Switch({ checked, onChange, disabled }: SwitchProps) { + return ( + <div> + <div className={`${SwitchClasses.container} default-colors`} style={{ + backgroundColor: checked ? SWITCH_ON : SWITCH_OFF, + opacity: disabled ? 0.3 : 1 + }}> + <svg + className={SwitchClasses.slider + " vc-switch-slider"} + viewBox="0 0 28 20" + preserveAspectRatio="xMinYMid meet" + aria-hidden="true" + style={{ + transform: checked ? "translateX(12px)" : "translateX(-3px)", + }} + > + <rect fill="white" x="4" y="0" height="20" width="20" rx="10" /> + <svg viewBox="0 0 20 20" fill="none"> + {checked ? ( + <> + <path fill={SWITCH_ON} d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z" /> + <path fill={SWITCH_ON} d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z" /> + </> + ) : ( + <> + <path fill={SWITCH_OFF} d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z" /> + <path fill={SWITCH_OFF} d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z" /> + </> + )} + + </svg> + </svg> + <input + disabled={disabled} + type="checkbox" + className={SwitchClasses.input} + tabIndex={0} + checked={checked} + onChange={e => onChange(e.currentTarget.checked)} + /> + </div> + </div> + ); +} diff --git a/src/components/VencordSettings/BackupRestoreTab.tsx b/src/components/VencordSettings/BackupRestoreTab.tsx index f0730b345..2ea04527a 100644 --- a/src/components/VencordSettings/BackupRestoreTab.tsx +++ b/src/components/VencordSettings/BackupRestoreTab.tsx @@ -18,19 +18,14 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; +import { classes } from "@utils/misc"; import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync"; import { Button, Card, Forms, Margins, Text } from "@webpack/common"; function BackupRestoreTab() { return ( - <Forms.FormSection title="Settings Sync"> - <Card style={{ - backgroundColor: "var(--info-warning-background)", - borderColor: "var(--info-warning-foreground)", - color: "var(--info-warning-text)", - padding: "1em", - marginBottom: "0.5em", - }}> + <Forms.FormSection title="Settings Sync" className={Margins.marginTop16}> + <Card className={classes("vc-settings-card", "vc-backup-restore-card")}> <Flex flexDirection="column"> <strong>Warning</strong> <span>Importing a settings file will overwrite your current settings.</span> @@ -50,7 +45,7 @@ function BackupRestoreTab() { </Text> <Flex> <Button - onClick={uploadSettingsBackup} + onClick={() => uploadSettingsBackup()} size={Button.Sizes.SMALL} > Import Settings diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index b673c4b7f..b2cf85b6a 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -75,11 +75,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) { export default ErrorBoundary.wrap(function () { const settings = useSettings(); - const ref = React.useRef<HTMLTextAreaElement>(); + const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n")); function onBlur() { settings.themeLinks = [...new Set( - ref.current!.value + themeText .trim() .split(/\n+/) .map(s => s.trim()) @@ -89,15 +89,11 @@ export default ErrorBoundary.wrap(function () { return ( <> - <Card style={{ - padding: "1em", - marginBottom: "1em", - marginTop: "1em" - }}> + <Card className="vc-settings-card"> <Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle> <Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText> - <Forms.FormDivider /> + <Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} /> <Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle> <div style={{ marginBottom: ".5em" }}> <Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes"> @@ -123,8 +119,8 @@ export default ErrorBoundary.wrap(function () { padding: ".5em", border: "1px solid var(--background-modifier-accent)" }} - ref={ref} - defaultValue={settings.themeLinks.join("\n")} + value={themeText} + onChange={e => setThemeText(e.currentTarget.value)} className={TextAreaProps.textarea} placeholder="Theme Links" spellCheck={false} diff --git a/src/components/VencordSettings/Updater.tsx b/src/components/VencordSettings/Updater.tsx index 33690697f..b5243f2d5 100644 --- a/src/components/VencordSettings/Updater.tsx +++ b/src/components/VencordSettings/Updater.tsx @@ -16,6 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { useSettings } from "@api/settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { ErrorCard } from "@components/ErrorCard"; import { Flex } from "@components/Flex"; @@ -23,7 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed"; import { Link } from "@components/Link"; import { classes, useAwaiter } from "@utils/misc"; import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater"; -import { Alerts, Button, Card, Forms, Margins, Parser, React, Toasts } from "@webpack/common"; +import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common"; import gitHash from "~git-hash"; @@ -69,14 +70,18 @@ interface CommonProps { repoPending: boolean; } +function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) { + return <Link href={`${repo}/commit/${hash}`} disabled={disabled}> + {hash} + </Link>; +} + function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) { return ( <Card style={{ padding: ".5em" }}> {updates.map(({ hash, author, message }) => ( <div> - <Link href={`${repo}/commit/${hash}`} disabled={repoPending}> - <code>{hash}</code> - </Link> + <code><HashLink {...{ repo, hash }} disabled={repoPending} /></code> <span style={{ marginLeft: "0.5em", color: "var(--text-normal)" @@ -179,6 +184,8 @@ function Newer(props: CommonProps) { } function Updater() { + const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]); + const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." }); React.useEffect(() => { @@ -192,16 +199,33 @@ function Updater() { }; return ( - <Forms.FormSection> + <Forms.FormSection className={Margins.marginTop16}> + <Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle> + <Switch + value={settings.notifyAboutUpdates} + onChange={(v: boolean) => settings.notifyAboutUpdates = v} + note="Shows a toast on startup" + disabled={settings.autoUpdate} + > + Get notified about new updates + </Switch> + <Switch + value={settings.autoUpdate} + onChange={(v: boolean) => settings.autoUpdate = v} + note="Automatically update Vencord without confirmation prompt" + > + Automatically update + </Switch> + <Forms.FormTitle tag="h5">Repo</Forms.FormTitle> <Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : ( <Link href={repo}> {repo.split("/").slice(-2).join("/")} </Link> - )} ({gitHash})</Forms.FormText> + )} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText> - <Forms.FormDivider /> + <Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} /> <Forms.FormTitle tag="h5">Updates</Forms.FormTitle> diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index df25e2d85..98808728a 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -18,31 +18,73 @@ import { useSettings } from "@api/settings"; +import { classNameFactory } from "@api/Styles"; import DonateButton from "@components/DonateButton"; import ErrorBoundary from "@components/ErrorBoundary"; +import { ErrorCard } from "@components/ErrorCard"; import IpcEvents from "@utils/IpcEvents"; -import { useAwaiter } from "@utils/misc"; -import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common"; +import { Margins } from "@utils/margins"; +import { identity, useAwaiter } from "@utils/misc"; +import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common"; -const st = (style: string) => `vcSettings${style}`; +const cl = classNameFactory("vc-settings-"); + +const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png"; +const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png"; + +type KeysOfType<Object, Type> = { + [K in keyof Object]: Object[K] extends Type ? K : never; +}[keyof Object]; function VencordSettings() { const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), { fallbackValue: "Loading..." }); const settings = useSettings(); + const notifSettings = settings.notifications; - const [donateImage] = React.useState( - Math.random() > 0.5 - ? "https://cdn.discordapp.com/emojis/1026533090627174460.png" - : "https://media.discordapp.net/stickers/1039992459209490513.png" - ); + const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []); + + const isWindows = navigator.platform.toLowerCase().startsWith("win"); + + const Switches: Array<false | { + key: KeysOfType<typeof settings, boolean>; + title: string; + note: string; + }> = + [ + { + key: "useQuickCss", + title: "Enable Custom CSS", + note: "Loads your Custom CSS" + }, + !IS_WEB && { + key: "enableReactDevtools", + title: "Enable React Developer Tools", + note: "Requires a full restart" + }, + !IS_WEB && !isWindows && { + key: "frameless", + title: "Disable the window frame", + note: "Requires a full restart" + }, + !IS_WEB && { + key: "transparent", + title: "Enable window transparency", + note: "Requires a full restart" + }, + !IS_WEB && isWindows && { + key: "winCtrlQ", + title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)", + note: "Requires a full restart" + } + ]; return ( <React.Fragment> <DonateCard image={donateImage} /> <Forms.FormSection title="Quick Actions"> - <Card className={st("QuickActionCard")}> + <Card className={cl("quick-actions-card")}> {IS_WEB ? ( <Button onClick={() => require("../Monaco").launchMonacoEditor()} @@ -82,34 +124,76 @@ function VencordSettings() { <Forms.FormDivider /> - <Forms.FormSection title="Settings"> - <Forms.FormText className={Margins.marginBottom20}> + <Forms.FormSection className={Margins.top16} title="Settings" tag="h5"> + <Forms.FormText className={Margins.bottom20}> Hint: You can change the position of this settings section in the settings of the "Settings" plugin! </Forms.FormText> - <Switch - value={settings.useQuickCss} - onChange={(v: boolean) => settings.useQuickCss = v} - note="Loads styles from your QuickCSS file"> - Use QuickCSS - </Switch> - {!IS_WEB && ( - <React.Fragment> - <Switch - value={settings.enableReactDevtools} - onChange={(v: boolean) => settings.enableReactDevtools = v} - note="Requires a full restart"> - Enable React Developer Tools - </Switch> - <Switch - value={settings.notifyAboutUpdates} - onChange={(v: boolean) => settings.notifyAboutUpdates = v} - note="Shows a toast on startup"> - Get notified about new updates - </Switch> - </React.Fragment> - )} - + {Switches.map(s => s && ( + <Switch + key={s.key} + value={settings[s.key]} + onChange={v => settings[s.key] = v} + note={s.note} + > + {s.title} + </Switch> + ))} </Forms.FormSection> + + + <Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle> + {notifSettings.useNative !== "never" && Notification.permission === "denied" && ( + <ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}> + <Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle> + <Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText> + </ErrorCard> + )} + <Forms.FormText className={Margins.bottom8}> + Some plugins may show you notifications. These come in two styles: + <ul> + <li><strong>Vencord Notifications</strong>: These are in-app notifications</li> + <li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li> + </ul> + </Forms.FormText> + <Select + placeholder="Notification Style" + options={[ + { label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true }, + { label: "Always use Desktop notifications", value: "always" }, + { label: "Always use Vencord notifications", value: "never" }, + ]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>} + closeOnSelect={true} + select={v => notifSettings.useNative = v} + isSelected={v => v === notifSettings.useNative} + serialize={identity} + /> + + <Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle> + <Select + isDisabled={notifSettings.useNative === "always"} + placeholder="Notification Position" + options={[ + { label: "Bottom Right", value: "bottom-right", default: true }, + { label: "Top Right", value: "top-right" }, + ]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>} + select={v => notifSettings.position = v} + isSelected={v => v === notifSettings.position} + serialize={identity} + /> + + <Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle> + <Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText> + <Slider + disabled={notifSettings.useNative === "always"} + markers={[0, 1000, 2500, 5000, 10_000, 20_000]} + minValue={0} + maxValue={20_000} + initialValue={notifSettings.timeout} + onValueChange={v => notifSettings.timeout = v} + onValueRender={v => (v / 1000).toFixed(2) + "s"} + onMarkerRender={v => (v / 1000) + "s"} + stickToMarkers={false} + /> </React.Fragment> ); } @@ -121,18 +205,10 @@ interface DonateCardProps { function DonateCard({ image }: DonateCardProps) { return ( - <Card style={{ - padding: "1em", - display: "flex", - flexDirection: "row", - marginBottom: "1em", - marginTop: "1em" - }}> + <Card className={cl("card", "donate")}> <div> <Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle> - <Forms.FormText> - Please consider supporting the development of Vencord by donating! - </Forms.FormText> + <Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText> <DonateButton style={{ transform: "translateX(-1em)" }} /> </div> <img @@ -140,7 +216,7 @@ function DonateCard({ image }: DonateCardProps) { src={image} alt="" height={128} - style={{ marginLeft: "auto", transform: "rotate(10deg)" }} + style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }} /> </Card> ); diff --git a/src/components/VencordSettings/index.tsx b/src/components/VencordSettings/index.tsx index b3a3322eb..acd81c36a 100644 --- a/src/components/VencordSettings/index.tsx +++ b/src/components/VencordSettings/index.tsx @@ -18,9 +18,10 @@ import "./settingsStyles.css"; +import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { findByCodeLazy } from "@webpack"; -import { Forms, Router, Text } from "@webpack/common"; +import { Forms, SettingsRouter, Text } from "@webpack/common"; import BackupRestoreTab from "./BackupRestoreTab"; import PluginsTab from "./PluginsTab"; @@ -28,7 +29,7 @@ import ThemesTab from "./ThemesTab"; import Updater from "./Updater"; import VencordSettings from "./VencordTab"; -const st = (style: string) => `vcSettings${style}`; +const cl = classNameFactory("vc-settings-"); const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]'); @@ -62,15 +63,15 @@ function Settings(props: SettingsProps) { <TabBar type={TabBar.Types.TOP} look={TabBar.Looks.BRAND} - className={st("TabBar")} + className={cl("tab-bar")} selectedItem={tab} - onItemSelect={Router.open} + onItemSelect={SettingsRouter.open} > {Object.entries(SettingsTabs).map(([key, { name, component }]) => { if (!component) return null; return <TabBar.Item id={key} - className={st("TabBarItem")} + className={cl("tab-bar-item")} key={key}> {name} </TabBar.Item>; diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css index e90ef141b..76064be14 100644 --- a/src/components/VencordSettings/settingsStyles.css +++ b/src/components/VencordSettings/settingsStyles.css @@ -1,23 +1,40 @@ -.vcSettingsTabBar { +.vc-settings-tab-bar { margin-top: 20px; margin-bottom: -2px; border-bottom: 2px solid var(--background-modifier-accent); } -.vcSettingsTabBarItem { +.vc-settings-tab-bar-item { margin-right: 32px; padding-bottom: 16px; margin-bottom: -2px; } -.vcSettingsQuickActionCard { +.vc-settings-quick-actions-card { padding: 1em; display: flex; gap: 1em; align-items: center; justify-content: space-between; - flex-wrap: wrap; flex-grow: 1; - flex-direction: row; + flex-flow: row wrap; margin-bottom: 1em; } + +.vc-settings-donate { + display: flex; + flex-direction: row; +} + +.vc-settings-card { + padding: 1em; + margin-bottom: 1em; + margin-top: 1em; +} + +.vc-backup-restore-card { + background-color: var(--info-warning-background); + border-color: var(--info-warning-foreground); + color: var(--info-warning-text); + margin-top: 0; +} diff --git a/src/ipcMain/extensions.ts b/src/ipcMain/extensions.ts index 0e26ff1d8..d8f843774 100644 --- a/src/ipcMain/extensions.ts +++ b/src/ipcMain/extensions.ts @@ -67,9 +67,18 @@ export async function installExt(id: string) { try { await access(extDir, fsConstants.F_OK); } catch (err) { - const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`; - const buf = await get(url); - await extract(crxToZip(buf), extDir); + const url = id === "fmkadmapgofadopljbjfkapdkoienihi" + // React Devtools v4.25 + // v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843 + // Unfortunately, Google does not serve old versions, so this is the only way + ? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip" + : `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`; + const buf = await get(url, { + headers: { + "User-Agent": "Vencord (https://github.com/Vendicated/Vencord)" + } + }); + await extract(crxToZip(buf), extDir).catch(console.error); } session.defaultSession.loadExtension(extDir); diff --git a/src/ipcMain/index.ts b/src/ipcMain/index.ts index ae8a96db0..6fb31d174 100644 --- a/src/ipcMain/index.ts +++ b/src/ipcMain/index.ts @@ -16,13 +16,12 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import "./legacy"; import "./updater"; import { debounce } from "@utils/debounce"; import IpcEvents from "@utils/IpcEvents"; import { Queue } from "@utils/Queue"; -import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron"; +import { BrowserWindow, ipcMain, shell } from "electron"; import { mkdirSync, readFileSync, watch } from "fs"; import { open, readFile, writeFile } from "fs/promises"; import { join } from "path"; @@ -45,9 +44,6 @@ export function readSettings() { } } -// Fix for screensharing in Electron >= 17 -ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts)); - ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH)); ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { @@ -81,7 +77,7 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => { export function initIpc(mainWindow: BrowserWindow) { open(QUICKCSS_PATH, "a+").then(fd => { fd.close(); - watch(QUICKCSS_PATH, debounce(async () => { + watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); }); @@ -95,7 +91,8 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { webPreferences: { preload: join(__dirname, "preload.js"), contextIsolation: true, - nodeIntegration: false + nodeIntegration: false, + sandbox: false } }); await win.loadURL(`data:text/html;base64,${monacoHtml}`); diff --git a/src/ipcMain/updater/git.ts b/src/ipcMain/updater/git.ts index 20cc5b1f0..89c2d3ccf 100644 --- a/src/ipcMain/updater/git.ts +++ b/src/ipcMain/updater/git.ts @@ -28,7 +28,9 @@ const VENCORD_SRC_DIR = join(__dirname, ".."); const execFile = promisify(cpExecFile); -const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord")); +const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord")); + +if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`; function git(...args: string[]) { const opts = { cwd: VENCORD_SRC_DIR }; @@ -66,10 +68,10 @@ async function pull() { async function build() { const opts = { cwd: VENCORD_SRC_DIR }; - let res; + const command = isFlatpak ? "flatpak-spawn" : "node"; + const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"]; - if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts); - else res = await execFile("node", ["scripts/build/build.mjs"], opts); + const res = await execFile(command, args, opts); return !res.stderr.includes("Build failed"); } diff --git a/src/ipcMain/updater/http.ts b/src/ipcMain/updater/http.ts index 3b3814477..cc106316f 100644 --- a/src/ipcMain/updater/http.ts +++ b/src/ipcMain/updater/http.ts @@ -37,10 +37,7 @@ async function githubGet(endpoint: string) { Accept: "application/vnd.github+json", // "All API requests MUST include a valid User-Agent header. // Requests with no User-Agent header will be rejected." - "User-Agent": VENCORD_USER_AGENT, - // todo: perhaps add support for (optional) api token? - // unauthorised rate limit is 60 reqs/h - // https://github.com/settings/tokens/new?description=Vencord%20Updater + "User-Agent": VENCORD_USER_AGENT } }); } @@ -52,7 +49,7 @@ async function calculateGitChanges() { const res = await githubGet(`/compare/${gitHash}...HEAD`); const data = JSON.parse(res.toString("utf-8")); - return data.commits.map(c => ({ + return data.commits.map((c: any) => ({ // github api only sends the long sha hash: c.sha.slice(0, 7), author: c.author.login, diff --git a/src/modules.d.ts b/src/modules.d.ts index c1a1996e7..d75a84f74 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -38,7 +38,8 @@ declare module "~fileContent/*" { export default content; } -declare module "*.css" { } +declare module "*.css"; + declare module "*.css?managed" { const name: string; export default name; diff --git a/src/patchWin32Updater.ts b/src/patchWin32Updater.ts index e853ebf4d..e08e37dc0 100644 --- a/src/patchWin32Updater.ts +++ b/src/patchWin32Updater.ts @@ -17,7 +17,7 @@ */ import { app, autoUpdater } from "electron"; -import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs"; +import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs"; import { basename, dirname, join } from "path"; const { setAppUserModelId } = app; @@ -44,58 +44,50 @@ function isNewer($new: string, old: string) { } function patchLatest() { - const currentAppPath = dirname(process.execPath); - const currentVersion = basename(currentAppPath); - const discordPath = join(currentAppPath, ".."); + try { + const currentAppPath = dirname(process.execPath); + const currentVersion = basename(currentAppPath); + const discordPath = join(currentAppPath, ".."); - const latestVersion = readdirSync(discordPath).reduce((prev, curr) => { - return (curr.startsWith("app-") && isNewer(curr, prev)) - ? curr - : prev; - }, currentVersion as string); + const latestVersion = readdirSync(discordPath).reduce((prev, curr) => { + return (curr.startsWith("app-") && isNewer(curr, prev)) + ? curr + : prev; + }, currentVersion as string); - if (latestVersion === currentVersion) return; + if (latestVersion === currentVersion) return; - const app = join(discordPath, latestVersion, "resources", "app"); - if (existsSync(app)) return; + const resources = join(discordPath, latestVersion, "resources"); + const app = join(resources, "app.asar"); + const _app = join(resources, "_app.asar"); - console.info("[Vencord] Detected Host Update. Repatching..."); + if (!existsSync(app) || statSync(app).isDirectory()) return; - const patcherPath = join(__dirname, "patcher.js"); - mkdirSync(app); - writeFileSync(join(app, "package.json"), JSON.stringify({ - name: "discord", - main: "index.js" - })); - writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`); + console.info("[Vencord] Detected Host Update. Repatching..."); + + renameSync(app, _app); + mkdirSync(app); + writeFileSync(join(app, "package.json"), JSON.stringify({ + name: "discord", + main: "index.js" + })); + writeFileSync(join(app, "index.js"), `require(${JSON.stringify(join(__dirname, "patcher.js"))});`); + } catch (err) { + console.error("[Vencord] Failed to repatch latest host update", err); + } } // Windows Host Updates install to a new folder app-{HOST_VERSION}, so we // need to reinject function patchUpdater() { - const main = require.main!; - const buildInfo = require(join(process.resourcesPath, "build_info.json")); - try { - if (buildInfo?.newUpdater) { - const autoStartScript = join(main.filename, "..", "autoStart", "win32.js"); - const { update } = require(autoStartScript); + const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js"); + const { update } = require(autoStartScript); - // New Updater Injection - require.cache[autoStartScript]!.exports.update = function () { - patchLatest(); - update.apply(this, arguments); - }; - } else { - const hostUpdaterScript = join(main.filename, "..", "hostUpdater.js"); - const { quitAndInstall } = require(hostUpdaterScript); - - // Old Updater Injection - require.cache[hostUpdaterScript]!.exports.quitAndInstall = function () { - patchLatest(); - quitAndInstall.apply(this, arguments); - }; - } + require.cache[autoStartScript]!.exports.update = function () { + update.apply(this, arguments); + patchLatest(); + }; } catch { // OpenAsar uses electrons autoUpdater on Windows const { quitAndInstall } = autoUpdater; diff --git a/src/patcher.ts b/src/patcher.ts index 0cf7e24ce..d51405d1b 100644 --- a/src/patcher.ts +++ b/src/patcher.ts @@ -17,8 +17,7 @@ */ import { onceDefined } from "@utils/onceDefined"; -import electron, { app, BrowserWindowConstructorOptions } from "electron"; -import { readFileSync } from "fs"; +import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron"; import { dirname, join } from "path"; import { initIpc } from "./ipcMain"; @@ -43,16 +42,48 @@ require.main!.filename = join(asarPath, discordPkg.main); app.setAppPath(asarPath); if (!process.argv.includes("--vanilla")) { + let settings: typeof import("@api/settings").Settings = {} as any; + try { + settings = JSON.parse(readSettings()); + } catch { } + // Repatch after host updates on Windows - if (process.platform === "win32") + if (process.platform === "win32") { require("./patchWin32Updater"); + if (settings.winCtrlQ) { + const originalBuild = Menu.buildFromTemplate; + Menu.buildFromTemplate = function (template) { + if (template[0]?.label === "&File") { + const { submenu } = template[0]; + if (Array.isArray(submenu)) { + submenu.push({ + label: "Quit (Hidden)", + visible: false, + acceleratorWorksWhenHidden: true, + accelerator: "Control+Q", + click: () => app.quit() + }); + } + } + return originalBuild.call(this, template); + }; + } + } + class BrowserWindow extends electron.BrowserWindow { constructor(options: BrowserWindowConstructorOptions) { if (options?.webPreferences?.preload && options.title) { const original = options.webPreferences.preload; options.webPreferences.preload = join(__dirname, "preload.js"); options.webPreferences.sandbox = false; + if (settings.frameless) { + options.frame = false; + } + if (settings.transparent) { + options.transparent = true; + options.backgroundColor = "#00000000"; + } process.env.DISCORD_PRELOAD = original; @@ -100,8 +131,7 @@ if (!process.argv.includes("--vanilla")) { }); try { - const settings = JSON.parse(readSettings()); - if (settings.enableReactDevtools) + if (settings?.enableReactDevtools) installExt("fmkadmapgofadopljbjfkapdkoienihi") .then(() => console.info("[Vencord] Installed React Developer Tools")) .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); @@ -160,21 +190,4 @@ if (!process.argv.includes("--vanilla")) { } console.log("[Vencord] Loading original Discord app.asar"); -// Legacy Vencord Injector requires "../app.asar". However, because we -// restore the require.main above this is messed up, so monkey patch Module._load to -// redirect such requires -// FIXME: remove this eventually -if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) { - console.warn("[Vencord] [--> WARNING <--] You have a legacy Vencord install. Please reinject"); - const Module = require("module"); - const loadModule = Module._load; - Module._load = function (path: string) { - if (path === "../app.asar") { - Module._load = loadModule; - arguments[0] = require.main!.filename; - } - return loadModule.apply(this, arguments); - }; -} else { - require(require.main!.filename); -} +require(require.main!.filename); diff --git a/src/plugins/alwaysTrust.ts b/src/plugins/alwaysTrust.ts new file mode 100644 index 000000000..8dde07fbf --- /dev/null +++ b/src/plugins/alwaysTrust.ts @@ -0,0 +1,42 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "AlwaysTrust", + description: "Removes the annoying untrusted domain and suspicious file popup", + authors: [Devs.zt], + patches: [ + { + find: ".displayName=\"MaskedLinkStore\"", + replacement: { + match: /\.isTrustedDomain=function\(.\){return.+?};/, + replace: ".isTrustedDomain=function(){return true};" + } + }, + { + find: "\"github.com\":new RegExp(\"\\\\/releases\\\\S*\\\\/download\"),", + replacement: { + match: /const o=JSON.parse\('\[.+?'\)/, + replace: "const o=[]" + } + } + ] +}); diff --git a/src/plugins/anonymiseFileNames.ts b/src/plugins/anonymiseFileNames.ts index f953a144e..26c423c0a 100644 --- a/src/plugins/anonymiseFileNames.ts +++ b/src/plugins/anonymiseFileNames.ts @@ -36,7 +36,7 @@ export default definePlugin({ replacement: { match: /uploadFiles:(.{1,2}),/, replace: - "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=Vencord.Plugins.plugins.AnonymiseFileNames.anonymise(f.filename)),$1(...args)),", + "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),", }, }, ], diff --git a/src/plugins/apiBadges.tsx b/src/plugins/apiBadges.tsx index 72c19f376..3fe2f6b18 100644 --- a/src/plugins/apiBadges.tsx +++ b/src/plugins/apiBadges.tsx @@ -73,7 +73,7 @@ export default definePlugin({ replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,` }, { - match: /spacing:(\d{1,2}),children:(.{1,40}(.{1,2})\.jsx.+(.{1,2})\.onClick.+\)})},/, + match: /spacing:(\d{1,2}),children:(.{1,40}(\i)\.jsx.+?(\i)\.onClick.+?\)})},/, // if the badge provides it's own component, render that instead of an image // the badge also includes info about the user that has it (type BadgeUserArgs), which is why it's passed as props replace: (_, s, origBadgeComponent, React, badge) => diff --git a/src/plugins/apiCommands.ts b/src/plugins/apiCommands.ts index e92b904e7..2197b3075 100644 --- a/src/plugins/apiCommands.ts +++ b/src/plugins/apiCommands.ts @@ -50,10 +50,10 @@ export default definePlugin({ }, // Show plugin name instead of "Built-In" { - find: "().source,children", + find: ".source,children", replacement: { // ...children: p?.name - match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\(\)\.source,children:)[^}]+/, + match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\.source,children:)[^}]+/, replace: "$1.plugin||($&)" } } diff --git a/src/plugins/apiMessageAccessories.ts b/src/plugins/apiMessageAccessories.ts index 0b30b58e2..5bb13cfe1 100644 --- a/src/plugins/apiMessageAccessories.ts +++ b/src/plugins/apiMessageAccessories.ts @@ -25,9 +25,9 @@ export default definePlugin({ authors: [Devs.Cyn], patches: [ { - find: "_messageAttachmentToEmbedMedia", + find: ".Messages.REMOVE_ATTACHMENT_BODY", replacement: { - match: /(\(\)\.container\)?,children:)(\[[^\]]+\])(}\)\};return)/, + match: /(.container\)?,children:)(\[[^\]]+\])(}\)\};return)/, replace: (_, pre, accessories, post) => `${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`, }, diff --git a/src/plugins/apiMessageDecorations.ts b/src/plugins/apiMessageDecorations.ts index 47f03f3b4..a3b251830 100644 --- a/src/plugins/apiMessageDecorations.ts +++ b/src/plugins/apiMessageDecorations.ts @@ -27,7 +27,7 @@ export default definePlugin({ { find: ".withMentionPrefix", replacement: { - match: /(\(\).roleDot.{10,50}{children:.{1,2})}\)/, + match: /(.roleDot.{10,50}{children:.{1,2})}\)/, replace: "$1.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))})" } } diff --git a/src/plugins/apiMessagePopover.ts b/src/plugins/apiMessagePopover.ts index 95814e05f..7e297f273 100644 --- a/src/plugins/apiMessagePopover.ts +++ b/src/plugins/apiMessagePopover.ts @@ -22,12 +22,17 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "MessagePopoverAPI", description: "API to add buttons to message popovers.", - authors: [Devs.KingFish], + authors: [Devs.KingFish, Devs.Ven], patches: [{ find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", replacement: { - match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/, - replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3" + // foo && !bar ? createElement(blah,...makeElement(addReactionData)) + match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/, + replace: (m, bools, makeElement) => { + const msg = m.match(/message:(.{1,3}),/)?.[1]; + if (!msg) throw new Error("Could not find message variable"); + return `...(${bools}?Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}):[]),${m}`; + } } }], }); diff --git a/src/plugins/apiNotices.ts b/src/plugins/apiNotices.ts index c362f76db..8922aceed 100644 --- a/src/plugins/apiNotices.ts +++ b/src/plugins/apiNotices.ts @@ -34,8 +34,8 @@ export default definePlugin({ ";if(Vencord.Api.Notices.currentNotice)return false$&" }, { - match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/, - replace: '{if($1?.id=="VencordNotice")return ($1=null,Vencord.Api.Notices.nextNotice(),true);' + match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/, + replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);' } ] } diff --git a/src/plugins/betterGifAltText.ts b/src/plugins/betterGifAltText.ts index 686ef0a55..5e0191b9d 100644 --- a/src/plugins/betterGifAltText.ts +++ b/src/plugins/betterGifAltText.ts @@ -31,7 +31,7 @@ export default definePlugin({ replacement: { match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/, replace: - "Vencord.Plugins.plugins.BetterGifAltText.altify(e);$1", + "$self.altify(e);$1", }, }, { @@ -39,7 +39,7 @@ export default definePlugin({ replacement: { match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/, replace: - "?($1.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify($1))", + "?($1.alt='GIF',$self.altify($1))", }, }, ], diff --git a/src/plugins/BetterNotes.ts b/src/plugins/betterNotes.ts similarity index 100% rename from src/plugins/BetterNotes.ts rename to src/plugins/betterNotes.ts diff --git a/src/plugins/betterRoleDot.ts b/src/plugins/betterRoleDot.ts index 3b2ac39f8..6ef92a863 100644 --- a/src/plugins/betterRoleDot.ts +++ b/src/plugins/betterRoleDot.ts @@ -33,7 +33,7 @@ export default definePlugin({ find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z", replacement: { match: /viewBox:"0 0 20 20"/, - replace: "$&,onClick:()=>Vencord.Plugins.plugins.BetterRoleDot.copyToClipBoard(e.color),style:{cursor:'pointer'}", + replace: "$&,onClick:()=>$self.copyToClipBoard(e.color),style:{cursor:'pointer'}", }, }, { @@ -41,7 +41,7 @@ export default definePlugin({ all: true, predicate: () => Settings.plugins.BetterRoleDot.bothStyles, replacement: { - match: /"(?:username|dot)"===\w\b/g, + match: /"(?:username|dot)"===\w(?!\.\w)/g, replace: "true", }, }, diff --git a/src/plugins/blurNsfw.ts b/src/plugins/blurNsfw.ts index 552189776..b24ded93d 100644 --- a/src/plugins/blurNsfw.ts +++ b/src/plugins/blurNsfw.ts @@ -43,12 +43,12 @@ export default definePlugin({ patches: [ { - find: "().embedWrapper,embed", + find: ".embedWrapper,embed", replacement: [{ - match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\(\)\.embedWrapper)/g, + match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\.embedWrapper)/g, replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')" }, { - match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\(\)\.embedWrapper)/g, + match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g, replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')" }] } diff --git a/src/plugins/callTimer.tsx b/src/plugins/callTimer.tsx index 6a1d357cd..f745bf625 100644 --- a/src/plugins/callTimer.tsx +++ b/src/plugins/callTimer.tsx @@ -74,8 +74,8 @@ export default definePlugin({ patches: [{ find: ".renderConnectionStatus=", replacement: { - match: /(?<=renderConnectionStatus=.+\(\)\.channel,children:)\w/, - replace: "[$&, Vencord.Plugins.plugins.CallTimer.renderTimer(this.props.channel.id)]" + match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/, + replace: "[$&, $self.renderTimer(this.props.channel.id)]" } }], renderTimer(channelId: string) { diff --git a/src/plugins/colorSighted.ts b/src/plugins/colorSighted.ts new file mode 100644 index 000000000..659e341d9 --- /dev/null +++ b/src/plugins/colorSighted.ts @@ -0,0 +1,37 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "ColorSighted", + description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord", + authors: [Devs.lewisakura], + patches: [ + { + find: "Masks.STATUS_ONLINE", + replacement: { + // we can use global replacement here - these are specific to the status icons and are used nowhere else, + // so it keeps the patch and plugin small and simple + match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g, + replace: "Masks.STATUS_ONLINE" + } + } + ] +}); diff --git a/src/plugins/corruptMp4s.ts b/src/plugins/corruptMp4s.ts index 5ae25eef6..b9c3a1197 100644 --- a/src/plugins/corruptMp4s.ts +++ b/src/plugins/corruptMp4s.ts @@ -99,7 +99,7 @@ export default definePlugin({ const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4"); const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR"); const file = new File([buf], newName, { type: "video/mp4" }); - setImmediate(() => promptToUpload([file], ctx.channel, DRAFT_TYPE)); + setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10); } }] }); diff --git a/src/plugins/customRPC.tsx b/src/plugins/customRPC.tsx new file mode 100644 index 000000000..9a0901b76 --- /dev/null +++ b/src/plugins/customRPC.tsx @@ -0,0 +1,251 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { definePluginSettings } from "@api/settings"; +import { Link } from "@components/Link"; +import { Devs } from "@utils/constants"; +import { useAwaiter } from "@utils/misc"; +import definePlugin, { OptionType } from "@utils/types"; +import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; +import { + FluxDispatcher, + Forms, + GuildStore, + React, + SelectedChannelStore, + SelectedGuildStore, + UserStore +} from "@webpack/common"; + +const ActivityComponent = findByCodeLazy("onOpenGameProfile"); +const ActivityClassName = findByPropsLazy("activity", "buttonColor"); +const Colors = findByPropsLazy("profileColors"); + +// START yoinked from lastfm.tsx +const assetManager = mapMangledModuleLazy( + "getAssetImage: size must === [number, number] for Twitch", + { + getAsset: filters.byCode("apply("), + } +); + +async function getApplicationAsset(key: string): Promise<string> { + return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0]; +} + +interface ActivityAssets { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; +} + +interface Activity { + state: string; + details?: string; + timestamps?: { + start?: Number; + end?: Number; + }; + assets?: ActivityAssets; + buttons?: Array<string>; + name: string; + application_id: string; + metadata?: { + button_urls?: Array<string>; + }; + type: ActivityType; + flags: Number; +} + +enum ActivityType { + PLAYING = 0, + LISTENING = 2, + WATCHING = 3, + COMPETING = 5 +} +// END + +const strOpt = (description: string) => ({ + type: OptionType.STRING, + description, + onChange: setRpc +}) as const; + +const numOpt = (description: string) => ({ + type: OptionType.NUMBER, + description, + onChange: setRpc +}) as const; + +const choice = (label: string, value: any, _default?: Boolean) => ({ + label, + value, + default: _default +}) as const; + +const choiceOpt = (description: string, options) => ({ + type: OptionType.SELECT, + description, + onChange: setRpc, + options +}) as const; + + +const settings = definePluginSettings({ + appID: strOpt("The ID of the application for the rich presence."), + appName: strOpt("The name of the presence."), + details: strOpt("Line 1 of rich presence."), + state: strOpt("Line 2 of rich presence."), + type: choiceOpt("Type of presence", [ + choice("Playing", ActivityType.PLAYING, true), + choice("Listening", ActivityType.LISTENING), + choice("Watching", ActivityType.WATCHING), + choice("Competing", ActivityType.COMPETING) + ]), + startTime: numOpt("Unix Timestamp for beginning of activity."), + endTime: numOpt("Unix Timestamp for end of activity."), + imageBig: strOpt("Sets the big image to the specified image."), + imageBigTooltip: strOpt("Sets the tooltip text for the big image."), + imageSmall: strOpt("Sets the small image to the specified image."), + imageSmallTooltip: strOpt("Sets the tooltip text for the small image."), + buttonOneText: strOpt("The text for the first button"), + buttonOneURL: strOpt("The URL for the first button"), + buttonTwoText: strOpt("The text for the second button"), + buttonTwoURL: strOpt("The URL for the second button") +}); + +async function createActivity(): Promise<Activity | undefined> { + const { + appID, + appName, + details, + state, + type, + startTime, + endTime, + imageBig, + imageBigTooltip, + imageSmall, + imageSmallTooltip, + buttonOneText, + buttonOneURL, + buttonTwoText, + buttonTwoURL + } = settings.store; + + if (!appName) return; + + const activity: Activity = { + application_id: appID || "0", + name: appName, + state, + details, + type, + flags: 1 << 0, + }; + + if (startTime) { + activity.timestamps = { + start: startTime, + }; + if (endTime) { + activity.timestamps.end = endTime; + } + } + + if (buttonOneText) { + activity.buttons = [ + buttonOneText, + buttonTwoText + ].filter(Boolean); + + activity.metadata = { + button_urls: [ + buttonOneURL, + buttonTwoURL + ].filter(Boolean) + }; + } + + if (imageBig) { + activity.assets = { + large_image: await getApplicationAsset(imageBig), + large_text: imageBigTooltip + }; + } + + if (imageSmall) { + activity.assets = { + ...activity.assets, + small_image: await getApplicationAsset(imageSmall), + small_text: imageSmallTooltip + }; + } + + + for (const k in activity) { + if (k === "type") continue; // without type, the presence is considered invalid. + const v = activity[k]; + if (!v || v.length === 0) + delete activity[k]; + } + + // WHAT DO YOU WANT FROM ME + // eslint-disable-next-line consistent-return + return activity; +} + +async function setRpc(disable?: Boolean) { + const activity: Activity | undefined = await createActivity(); + + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity: !disable ? activity : {} + }); +} + +export default definePlugin({ + name: "CustomRPC", + description: "Allows you to set a custom rich presence.", + authors: [Devs.captain], + start: setRpc, + stop: () => setRpc(true), + settings, + + settingsAboutComponent: () => { + const activity = useAwaiter(createActivity); + return ( + <> + <Forms.FormTitle tag="h2">NOTE:</Forms.FormTitle> + <Forms.FormText> + You will need to <Link href="https://discord.com/developers/applications">create an + application</Link> and + get its ID to use this plugin. + </Forms.FormText> + <Forms.FormDivider /> + <div style={{ width: "284px" }} className={Colors.profileColors}> + {activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()} + guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())} + application={{ id: settings.store.appID }} + user={UserStore.getCurrentUser()} />} + </div> + </> + ); + } +}); diff --git a/src/plugins/DisableDMCallIdle.ts b/src/plugins/disableDMCallIdle.ts similarity index 100% rename from src/plugins/DisableDMCallIdle.ts rename to src/plugins/disableDMCallIdle.ts diff --git a/src/plugins/emoteCloner.tsx b/src/plugins/emoteCloner.tsx index c22eebdc5..15d13588d 100644 --- a/src/plugins/emoteCloner.tsx +++ b/src/plugins/emoteCloner.tsx @@ -57,7 +57,7 @@ async function doClone(guildId: string, id: string, name: string, isAnimated: bo reader.onload = () => { uploadEmoji({ guildId, - name, + name: name.split("~")[0], image: reader.result }).then(() => { Toasts.show({ @@ -187,7 +187,7 @@ export default definePlugin({ find: "open-native-link", replacement: { match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/, - replace: "$&,Vencord.Plugins.plugins.EmoteCloner.makeMenu(arguments[2])" + replace: "$&,$self.makeMenu(arguments[2])" }, }, diff --git a/src/plugins/fakeNitro.ts b/src/plugins/fakeNitro.ts index e5ac3b9ce..22ffb8009 100644 --- a/src/plugins/fakeNitro.ts +++ b/src/plugins/fakeNitro.ts @@ -22,11 +22,25 @@ import { Devs } from "@utils/constants"; import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies"; import definePlugin, { OptionType } from "@utils/types"; import { findByCodeLazy, findByPropsLazy } from "@webpack"; -import { ChannelStore, UserStore } from "@webpack/common"; +import { ChannelStore, PermissionStore, UserStore } from "@webpack/common"; const DRAFT_TYPE = 0; const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); +const USE_EXTERNAL_EMOJIS = 1n << 18n; +const USE_EXTERNAL_STICKERS = 1n << 37n; + +enum EmojiIntentions { + REACTION = 0, + STATUS = 1, + COMMUNITY_CONTENT = 2, + CHAT = 3, + GUILD_STICKER_RELATED_EMOJI = 4, + GUILD_ROLE_BENEFIT_EMOJI = 5, + COMMUNITY_CONTENT_ONLY = 6, + SOUNDBOARD = 7 +} + interface BaseSticker { available: boolean; description: string; @@ -58,26 +72,39 @@ migratePluginSettings("FakeNitro", "NitroBypass"); export default definePlugin({ name: "FakeNitro", - authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity], - description: "Allows you to stream in nitro quality and send fake emojis/stickers.", + authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain], + description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.", dependencies: ["MessageEventsAPI"], patches: [ { - find: "canUseAnimatedEmojis:function", + find: ".PREMIUM_LOCKED;", predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true, replacement: [ - "canUseAnimatedEmojis", - "canUseEmojisEverywhere" - ].map(func => { - return { - match: new RegExp(`${func}:function\\(.+?\\{`), - replace: "$&return true;" - }; - }) + { + match: /(?<=(?<intention>\i)=\i\.intention)/, + replace: ",fakeNitroIntention=$<intention>" + }, + { + match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g, + replace: ",fakeNitroIntention" + }, + { + match: /(?<=&&!\i&&)!(?<canUseExternal>\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, + replace: `(!$<canUseExternal>&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))` + } + ] }, { find: "canUseAnimatedEmojis:function", + predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true, + replacement: { + match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?<user>\i))\){(?<premiumCheck>.+?\))/g, + replace: `,fakeNitroIntention){$<premiumCheck>||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` + } + }, + { + find: "canUseStickersEverywhere:function", predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true, replacement: { match: /canUseStickersEverywhere:function\(.+?\{/, @@ -93,7 +120,7 @@ export default definePlugin({ } }, { - find: "canUseAnimatedEmojis:function", + find: "canStreamHighQuality:function", predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true, replacement: [ "canUseHighVideoUploadQuality", @@ -114,6 +141,13 @@ export default definePlugin({ replace: "" } }, + { + find: "canUseClientThemes:function", + replacement: { + match: /(?<=canUseClientThemes:function\(\i\){)/, + replace: "return true;" + } + } ], options: { @@ -161,6 +195,22 @@ export default definePlugin({ return (UserStore.getCurrentUser().premiumType ?? 0) > 1; }, + hasPermissionToUseExternalEmojis(channelId: string) { + const channel = ChannelStore.getChannel(channelId); + + if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true; + + return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel); + }, + + hasPermissionToUseExternalStickers(channelId: string) { + const channel = ChannelStore.getChannel(channelId); + + if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true; + + return PermissionStore.can(USE_EXTERNAL_STICKERS, channel); + }, + getStickerLink(stickerId: string) { return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`; }, @@ -245,7 +295,7 @@ export default definePlugin({ if (!sticker) break stickerBypass; - if (sticker.available !== false && (this.canUseStickers || (sticker as GuildSticker)?.guild_id === guildId)) + if (sticker.available !== false && ((this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId)) || (sticker as GuildSticker)?.guild_id === guildId)) break stickerBypass; let link = this.getStickerLink(sticker.id); @@ -268,7 +318,7 @@ export default definePlugin({ } } - if (!this.canUseEmotes && settings.enableEmojiBypass) { + if ((!this.canUseEmotes || !this.hasPermissionToUseExternalEmojis(channelId)) && settings.enableEmojiBypass) { for (const emoji of messageObj.validNonShortcutEmojis) { if (!emoji.require_colons) continue; if (emoji.guildId === guildId && !emoji.animated) continue; @@ -284,22 +334,22 @@ export default definePlugin({ return { cancel: false }; }); - if (!this.canUseEmotes && settings.enableEmojiBypass) { - this.preEdit = addPreEditListener((_, __, messageObj) => { - const { guildId } = this; + this.preEdit = addPreEditListener((channelId, __, messageObj) => { + if (this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId)) return; - for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) { - const emoji = EmojiStore.getCustomEmojiById(emojiId); - if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue; - if (!emoji.require_colons) continue; + const { guildId } = this; - const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`); - messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; - }); - } - }); - } + for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) { + const emoji = EmojiStore.getCustomEmojiById(emojiId); + if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue; + if (!emoji.require_colons) continue; + + const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`); + messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => { + return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; + }); + } + }); }, stop() { diff --git a/src/plugins/forceOwnerCrown.ts b/src/plugins/forceOwnerCrown.ts index 9345ea75b..3122410f9 100644 --- a/src/plugins/forceOwnerCrown.ts +++ b/src/plugins/forceOwnerCrown.ts @@ -18,10 +18,7 @@ import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { waitFor } from "@webpack"; - -let GuildStore; -waitFor(["getGuild"], m => GuildStore = m); +import { GuildStore } from "@webpack/common"; export default definePlugin({ name: "ForceOwnerCrown", @@ -33,7 +30,7 @@ export default definePlugin({ find: ".renderOwner=", replacement: { match: /isOwner;return null!=(\w+)?&&/g, - replace: "isOwner;if(Vencord.Plugins.plugins.ForceOwnerCrown.isGuildOwner(this.props)){$1=true;}return null!=$1&&" + replace: "isOwner;if($self.isGuildOwner(this.props)){$1=true;}return null!=$1&&" } }, ], diff --git a/src/plugins/ignoreActivities.tsx b/src/plugins/ignoreActivities.tsx index 981145c68..56a0a8dba 100644 --- a/src/plugins/ignoreActivities.tsx +++ b/src/plugins/ignoreActivities.tsx @@ -35,7 +35,7 @@ interface IgnoredActivity { } const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn"); -const PreviewBadgeClasses = findByPropsLazy("previewBadge", "previewBadgeIcon"); +const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon"); const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight"); const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen"); @@ -116,7 +116,7 @@ function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) { function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) { return ( <div - className={`${PreviewBadgeClasses.previewBadge} ${BaseShapeRoundClasses.baseShapeRound}`} + className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`} style={{ padding: "0 2px" }} > <ToggleActivityComponent activity={activity} /> @@ -143,22 +143,22 @@ export default definePlugin({ authors: [Devs.Nuckyz], description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.", patches: [{ - find: ".Messages.SETTINGS_GAMES_OVERLAY_ON", + find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", replacement: { - match: /(this.renderLastPlayed\(\)]}\),this.renderOverlayToggle\(\))/, - replace: "$1,Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(this.props)" + match: /var .=(?<props>.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/, + replace: "$&,$self.renderToggleGameActivityButton($<props>)" } }, { - find: ".Messages.NEW,name", + find: ".overlayBadge", replacement: { - match: /\(\)\.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/, - replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleActivityButton($<props>)" + match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/, + replace: "$&,$self.renderToggleActivityButton($<props>)" } }, { find: '.displayName="LocalActivityStore"', replacement: { - match: /((.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?;)/, - replace: "$1$2=$2.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled);" + match: /(?<activities>.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?\)\)/, + replace: "$&;$<activities>=$<activities>.filter($self.isActivityNotIgnored);" } }], @@ -189,12 +189,10 @@ export default definePlugin({ } }, - renderToggleGameActivityButton(props: { game: { id?: string; exePath: string; } | null; }) { - if (!props.game) return (null); - + renderToggleGameActivityButton(props: { id?: string; exePath: string; }) { return ( <ErrorBoundary noop> - <ToggleActivityComponent activity={{ id: props.game.id ?? props.game.exePath, type: ActivitiesTypes.Game }} /> + <ToggleActivityComponent activity={{ id: props.id ?? props.exePath, type: ActivitiesTypes.Game }} /> </ErrorBoundary> ); }, @@ -207,7 +205,7 @@ export default definePlugin({ ); }, - isActivityEnabled(props: { type: number; application_id?: string; name?: string; }) { + isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) { if (props.type === 0) { if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id); else { diff --git a/src/plugins/index.ts b/src/plugins/index.ts index c0325d41d..6ac221da5 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -60,7 +60,16 @@ for (const p of pluginsValues) { }); } -for (const p of pluginsValues) +for (const p of pluginsValues) { + if (p.settings) { + p.settings.pluginName = p.name; + p.options ??= {}; + for (const [name, def] of Object.entries(p.settings.def)) { + const checks = p.settings.checks?.[name]; + p.options[name] = { ...def, ...checks }; + } + } + if (p.patches && isPluginEnabled(p.name)) { for (const patch of p.patches) { patch.plugin = p.name; @@ -69,6 +78,7 @@ for (const p of pluginsValues) patches.push(patch); } } +} export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins() { for (const name in Plugins) diff --git a/src/plugins/invisibleChat/components/DecryptionModal.tsx b/src/plugins/invisibleChat/components/DecryptionModal.tsx new file mode 100644 index 000000000..a17239bc5 --- /dev/null +++ b/src/plugins/invisibleChat/components/DecryptionModal.tsx @@ -0,0 +1,77 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { + ModalContent, + ModalFooter, + ModalHeader, + ModalRoot, + openModal, +} from "@utils/modal"; +import { Button, Forms, React, TextInput } from "@webpack/common"; + +import { decrypt } from "../index"; + +export function DecModal(props: any) { + const secret: string = props?.message?.content; + const [password, setPassword] = React.useState("password"); + + return ( + <ModalRoot {...props}> + <ModalHeader> + <Forms.FormTitle tag="h4">Decrypt Message</Forms.FormTitle> + </ModalHeader> + + <ModalContent> + <Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle> + <TextInput defaultValue={secret} disabled={true}></TextInput> + <Forms.FormTitle tag="h5">Password</Forms.FormTitle> + <TextInput + style={{ marginBottom: "20px" }} + onChange={setPassword} + /> + </ModalContent> + + <ModalFooter> + <Button + color={Button.Colors.GREEN} + onClick={() => { + const toSend = decrypt(secret, password, true); + if (!toSend || !props?.message) return; + // @ts-expect-error + Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend); + props.onClose(); + }}> + Decrypt + </Button> + <Button + color={Button.Colors.TRANSPARENT} + look={Button.Looks.LINK} + style={{ left: 15, position: "absolute" }} + onClick={props.onClose} + > + Cancel + </Button> + </ModalFooter> + </ModalRoot> + ); +} + +export function buildDecModal(msg: any): any { + openModal((props: any) => <DecModal {...props} {...msg} />); +} diff --git a/src/plugins/invisibleChat/components/EncryptionModal.tsx b/src/plugins/invisibleChat/components/EncryptionModal.tsx new file mode 100644 index 000000000..f650f28c5 --- /dev/null +++ b/src/plugins/invisibleChat/components/EncryptionModal.tsx @@ -0,0 +1,116 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { + ModalContent, + ModalFooter, + ModalHeader, + ModalProps, + ModalRoot, + openModal, +} from "@utils/modal"; +import { findLazy } from "@webpack"; +import { Button, Forms, React, Switch, TextInput } from "@webpack/common"; + +import { encrypt } from "../index"; + +const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT); + +function EncModal(props: ModalProps) { + const [secret, setSecret] = React.useState(""); + const [cover, setCover] = React.useState(""); + const [password, setPassword] = React.useState("password"); + const [noCover, setNoCover] = React.useState(false); + + const isValid = secret && (noCover || (cover && /\w \w/.test(cover))); + + return ( + <ModalRoot {...props}> + <ModalHeader> + <Forms.FormTitle tag="h4">Encrypt Message</Forms.FormTitle> + </ModalHeader> + + <ModalContent> + <Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle> + <TextInput + onChange={(e: string) => { + setSecret(e); + }} + /> + <Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Cover (2 or more Words!!)</Forms.FormTitle> + <TextInput + disabled={noCover} + onChange={(e: string) => { + setCover(e); + }} + /> + <Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle> + <TextInput + style={{ marginBottom: "20px" }} + defaultValue={"password"} + onChange={(e: string) => { + setPassword(e); + }} + /> + <Switch + value={noCover} + onChange={(e: boolean) => { + setNoCover(e); + }} + > + Don't use a Cover + </Switch> + </ModalContent> + + <ModalFooter> + <Button + color={Button.Colors.GREEN} + disabled={!isValid} + onClick={() => { + if (!isValid) return; + const encrypted = encrypt(secret, password, noCover ? "d d" : cover); + const toSend = noCover ? encrypted.replaceAll("d", "") : encrypted; + if (!toSend) return; + + ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { + rawText: `${toSend}` + }); + + props.onClose(); + }} + > + Send + </Button> + <Button + color={Button.Colors.TRANSPARENT} + look={Button.Looks.LINK} + style={{ left: 15, position: "absolute" }} + onClick={() => { + props.onClose(); + }} + > + Cancel + </Button> + </ModalFooter> + </ModalRoot> + ); +} + +export function buildEncModal(): any { + openModal(props => <EncModal {...props} />); +} diff --git a/src/plugins/invisibleChat/index.tsx b/src/plugins/invisibleChat/index.tsx new file mode 100644 index 000000000..a83d47b84 --- /dev/null +++ b/src/plugins/invisibleChat/index.tsx @@ -0,0 +1,257 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { addButton, removeButton } from "@api/MessagePopover"; +import { definePluginSettings } from "@api/settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { getStegCloak } from "@utils/dependencies"; +import definePlugin, { OptionType } from "@utils/types"; +import { Button, ButtonLooks, ButtonWrapperClasses, ChannelStore, FluxDispatcher, Tooltip } from "@webpack/common"; +import { Message } from "discord-types/general"; + +import { buildDecModal } from "./components/DecryptionModal"; +import { buildEncModal } from "./components/EncryptionModal"; + +let steggo: any; + +function PopOverIcon() { + return ( + + <svg + fill="var(--header-secondary)" + width={24} height={24} + viewBox={"0 0 64 64"} + > + <path d="M 32 9 C 24.832 9 19 14.832 19 22 L 19 27.347656 C 16.670659 28.171862 15 30.388126 15 33 L 15 49 C 15 52.314 17.686 55 21 55 L 43 55 C 46.314 55 49 52.314 49 49 L 49 33 C 49 30.388126 47.329341 28.171862 45 27.347656 L 45 22 C 45 14.832 39.168 9 32 9 z M 32 13 C 36.963 13 41 17.038 41 22 L 41 27 L 23 27 L 23 22 C 23 17.038 27.037 13 32 13 z" /> + </svg> + ); +} + + +function Indicator() { + return ( + <Tooltip text="This message has a hidden message! (InvisibleChat)"> + {({ onMouseEnter, onMouseLeave }) => ( + <img + aria-label="Hidden Message Indicator (InvisibleChat)" + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + src="https://github.com/SammCheese/invisible-chat/raw/NewReplugged/src/assets/lock.png" + width={20} + height={20} + style={{ transform: "translateY(4p)", paddingInline: 4 }} + /> + )} + </Tooltip> + + ); + +} + +function ChatBarIcon() { + return ( + <Tooltip text="Encrypt Message"> + {({ onMouseEnter, onMouseLeave }) => ( + // size="" = Button.Sizes.NONE + /* + many themes set "> button" to display: none, as the gift button is + the only directly descending button (all the other elements are divs.) + Thus, wrap in a div here to avoid getting hidden by that. + flex is for some reason necessary as otherwise the button goes flying off + */ + <div style={{ display: "flex" }}> + <Button + aria-haspopup="dialog" + aria-label="Encrypt Message" + size="" + look={ButtonLooks.BLANK} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + innerClassName={ButtonWrapperClasses.button} + onClick={() => buildEncModal()} + style={{ marginRight: "2px" }} + > + <div className={ButtonWrapperClasses.buttonWrapper}> + <svg + aria-hidden + role="img" + width="24" + height="24" + viewBox={"0 0 64 64"} + style={{ scale: "1.1" }} + > + <path fill="currentColor" d="M 32 9 C 24.832 9 19 14.832 19 22 L 19 27.347656 C 16.670659 28.171862 15 30.388126 15 33 L 15 49 C 15 52.314 17.686 55 21 55 L 43 55 C 46.314 55 49 52.314 49 49 L 49 33 C 49 30.388126 47.329341 28.171862 45 27.347656 L 45 22 C 45 14.832 39.168 9 32 9 z M 32 13 C 36.963 13 41 17.038 41 22 L 41 27 L 23 27 L 23 22 C 23 17.038 27.037 13 32 13 z" /> + </svg> + </div> + </Button> + </div> + ) + } + </Tooltip > + ); +} + +const settings = definePluginSettings({ + savedPasswords: { + type: OptionType.STRING, + default: "password, Password", + description: "Saved Passwords (Seperated with a , )" + } +}); + +export default definePlugin({ + name: "InvisibleChat", + description: "Encrypt your Messages in a non-suspicious way! This plugin makes requests to >>https://embed.sammcheese.net<< to provide embeds to decrypted links!", + authors: [Devs.SammCheese], + patches: [ + { + // Indicator + find: ".Messages.MESSAGE_EDITED,", + replacement: { + match: /var .,.,.=(.)\.className,.=.\.message,.=.\.children,.=.\.content,.=.\.onUpdate/gm, + replace: "try {$1 && $self.INV_REGEX.test($1.content[0]) ? $1.content.push($self.indicator()) : null } catch {};$&" + } + }, + { + find: ".activeCommandOption", + replacement: { + match: /.=.\.activeCommand,.=.\.activeCommandOption,.{1,133}(.)=\[\];/, + replace: "$&;$1.push($self.chatBarIcon());", + } + }, + ], + + EMBED_API_URL: "https://embed.sammcheese.net", + INV_REGEX: new RegExp(/( \u200c|\u200d |[\u2060-\u2064])[^\u200b]/), + URL_REGEX: new RegExp( + /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/, + ), + settings, + async start() { + const { default: StegCloak } = await getStegCloak(); + steggo = new StegCloak(true, false); + + addButton("invDecrypt", message => { + return this.INV_REGEX.test(message?.content) + ? { + label: "Decrypt Message", + icon: this.popOverIcon, + message: message, + channel: ChannelStore.getChannel(message.channel_id), + onClick: async () => { + await iteratePasswords(message).then((res: string | false) => { + if (res) return void this.buildEmbed(message, res); + return void buildDecModal({ message }); + }); + } + } + : null; + }); + }, + + stop() { + removeButton("invDecrypt"); + }, + + // Gets the Embed of a Link + async getEmbed(url: URL): Promise<Object | {}> { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + const options: RequestInit = { + signal: controller.signal, + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + url, + }), + }; + + // AWS hosted url to discord embed object + const rawRes = await fetch(this.EMBED_API_URL, options); + clearTimeout(timeout); + + return await rawRes.json(); + }, + + async buildEmbed(message: any, revealed: string): Promise<void> { + const urlCheck = revealed.match(this.URL_REGEX); + + message.embeds.push({ + type: "rich", + title: "Decrypted Message", + color: "0x45f5f5", + description: revealed, + footer: { + text: "Made with ❤️ by c0dine and Sammy!", + }, + }); + + if (urlCheck?.length) + message.embeds.push(await this.getEmbed(new URL(urlCheck[0]))); + + this.updateMessage(message); + }, + + updateMessage: (message: any) => { + FluxDispatcher.dispatch({ + type: "MESSAGE_UPDATE", + message, + }); + }, + + chatBarIcon: ErrorBoundary.wrap(ChatBarIcon, { noop: true }), + popOverIcon: () => <PopOverIcon />, + indicator: ErrorBoundary.wrap(Indicator, { noop: true }) +}); + +export function encrypt(secret: string, password: string, cover: string): string { + return steggo.hide(secret + "\u200b", password, cover); +} + +export function decrypt(secret: string, password: string, removeIndicator: boolean): string { + const decrypted = steggo.reveal(secret, password); + return removeIndicator ? decrypted.replace("\u200b", "") : decrypted; +} + +export function isCorrectPassword(result: string): boolean { + return result.endsWith("\u200b"); +} + +export async function iteratePasswords(message: Message): Promise<string | false> { + const passwords = settings.store.savedPasswords.split(",").map(s => s.trim()); + + if (!message?.content || !passwords?.length) return false; + + let { content } = message; + + // we use an extra variable so we dont have to edit the message content directly + if (/^\W/.test(message.content)) content = `d ${message.content}d`; + + for (let i = 0; i < passwords.length; i++) { + const result = decrypt(content, passwords[i], false); + if (isCorrectPassword(result)) { + return result; + } + } + + return false; +} diff --git a/src/plugins/loadingQuotes.ts b/src/plugins/loadingQuotes.ts index 052bfe6ec..7be6f3054 100644 --- a/src/plugins/loadingQuotes.ts +++ b/src/plugins/loadingQuotes.ts @@ -68,7 +68,7 @@ export default definePlugin({ find: ".LOADING_DID_YOU_KNOW", replacement: { match: /\._loadingText=.+?random\(.+?;/s, - replace: "._loadingText=Vencord.Plugins.plugins.LoadingQuotes.quote;", + replace: "._loadingText=$self.quote;", }, }, ], diff --git a/src/plugins/memberCount.tsx b/src/plugins/memberCount.tsx index 947d4d7b7..37626141a 100644 --- a/src/plugins/memberCount.tsx +++ b/src/plugins/memberCount.tsx @@ -56,7 +56,7 @@ function MemberCount() { <div {...props}> <span style={{ - backgroundColor: "var(--status-green-600)", + backgroundColor: "var(--green-360)", width: "12px", height: "12px", borderRadius: "50%", @@ -64,7 +64,7 @@ function MemberCount() { marginRight: "0.5em" }} /> - <span style={{ color: "var(--status-green-600)" }}>{online}</span> + <span style={{ color: "var(--green-360)" }}>{online}</span> </div> )} </Tooltip> @@ -76,13 +76,13 @@ function MemberCount() { width: "6px", height: "6px", borderRadius: "50%", - border: "3px solid var(--status-grey-500)", + border: "3px solid var(--primary-400)", display: "inline-block", marginRight: "0.5em", marginLeft: "1em" }} /> - <span style={{ color: "var(--status-grey-500)" }}>{total}</span> + <span style={{ color: "var(--primary-400)" }}>{total}</span> </div> )} </Tooltip> @@ -99,7 +99,7 @@ export default definePlugin({ find: ".isSidebarVisible,", replacement: { match: /(var (.)=.\.className.+?children):\[(.\.useMemo[^}]+"aria-multiselectable")/, - replace: "$1:[$2.startsWith('members')?Vencord.Plugins.plugins.MemberCount.render():null,$3" + replace: "$1:[$2.startsWith('members')?$self.render():null,$3" } }], diff --git a/src/plugins/messageActions.ts b/src/plugins/messageActions.ts index df4d01611..b71a9f162 100644 --- a/src/plugins/messageActions.ts +++ b/src/plugins/messageActions.ts @@ -1,6 +1,6 @@ /* * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors + * Copyright (c) 2023 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ import { addClickListener, removeClickListener } from "@api/MessageEvents"; import { migratePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy, findLazy } from "@webpack"; import { UserStore } from "@webpack/common"; @@ -35,6 +35,19 @@ export default definePlugin({ authors: [Devs.Ven], dependencies: ["MessageEventsAPI"], + options: { + enableDeleteOnClick: { + type: OptionType.BOOLEAN, + description: "Enable delete on click", + default: true + }, + enableDoubleClickToEdit: { + type: OptionType.BOOLEAN, + description: "Enable double click to edit", + default: true + } + }, + start() { const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); const PermissionStore = findByPropsLazy("can", "initialize"); @@ -47,11 +60,11 @@ export default definePlugin({ this.onClick = addClickListener((msg, chan, event) => { const isMe = msg.author.id === UserStore.getCurrentUser().id; if (!isDeletePressed) { - if (isMe && event.detail >= 2 && !EditStore.isEditing(chan.id, msg.id)) { + if (Vencord.Settings.plugins.MessageClickActions.enableDoubleClickToEdit && (isMe && event.detail >= 2 && !EditStore.isEditing(chan.id, msg.id))) { MessageActions.startEditMessage(chan.id, msg.id, msg.content); event.preventDefault(); } - } else if (isMe || PermissionStore.can(Permissions.MANAGE_MESSAGES, chan)) { + } else if (Vencord.Settings.plugins.MessageClickActions.enableDeleteOnClick && (isMe || PermissionStore.can(Permissions.MANAGE_MESSAGES, chan))) { MessageActions.deleteMessage(chan.id, msg.id); event.preventDefault(); } diff --git a/src/plugins/messageLinkEmbeds.tsx b/src/plugins/messageLinkEmbeds.tsx index f57b5d0e8..a6b63d82c 100644 --- a/src/plugins/messageLinkEmbeds.tsx +++ b/src/plugins/messageLinkEmbeds.tsx @@ -44,7 +44,7 @@ let AutomodEmbed: React.ComponentType<any>, Endpoints: Record<string, any>; waitFor(["mle_AutomodEmbed"], m => (AutomodEmbed = m.mle_AutomodEmbed)); -waitFor(filters.byCode("().inlineMediaEmbed"), m => Embed = m); +waitFor(filters.byCode(".inlineMediaEmbed"), m => Embed = m); waitFor(m => m.type?.toString()?.includes('["message","compact","className",'), m => ChannelMessage = m); waitFor(["MESSAGE_CREATE_ATTACHMENT_UPLOAD"], _ => Endpoints = _); const SearchResultClasses = findByPropsLazy("message", "searchResult"); @@ -139,6 +139,16 @@ interface MessageEmbedProps { guildID: string; } +function withEmbeddedBy(message: Message, embeddedBy: string[]) { + return new Proxy(message, { + get(_, prop) { + if (prop === "vencordEmbeddedBy") return embeddedBy; + // @ts-ignore ts so bad + return Reflect.get(...arguments); + } + }); +} + export default definePlugin({ name: "MessageLinkEmbeds", description: "Adds a preview to messages that link another message", @@ -146,14 +156,13 @@ export default definePlugin({ dependencies: ["MessageAccessoriesAPI"], patches: [ { - find: "().embedCard", + find: ".embedCard", replacement: [{ match: /{"use strict";(.{0,10})\(\)=>(.{1,2})}\);/, replace: '{"use strict";$1()=>$2,me:()=>messageEmbed});' }, { - match: /function (.{1,2})\((.{1,2})\){var (.{1,2})=.{1,2}\.message,(.{1,2})=.{1,2}\.channel(.{0,300})\(\)\.embedCard(.{0,500})}\)}/, - replace: "function $1($2){var $3=$2.message,$4=$2.channel$5().embedCard$6})}\ -var messageEmbed={mle_AutomodEmbed:$1};" + match: /function (.{1,2})\(.{1,2}\){var .{1,2}=.{1,2}\.message,.{1,2}=.{1,2}\.channel.{0,300}\.embedCard.{0,500}}\)}/, + replace: "$&;var messageEmbed={mle_AutomodEmbed:$1};" }] } ], @@ -195,19 +204,24 @@ var messageEmbed={mle_AutomodEmbed:$1};" messageEmbedAccessory(props) { const { message }: { message: Message; } = props; + // @ts-ignore + const embeddedBy: string[] = message.vencordEmbeddedBy ?? []; const accessories = [] as (JSX.Element | null)[]; let match = null as RegExpMatchArray | null; while ((match = this.messageLinkRegex.exec(message.content!)) !== null) { const [_, guildID, channelID, messageID] = match; + if (embeddedBy.includes(messageID)) { + continue; + } const linkedChannel = ChannelStore.getChannel(channelID); if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) { continue; } - let linkedMessage = messageCache[messageID]?.message as Message; + let linkedMessage = messageCache[messageID]?.message; if (!linkedMessage) { linkedMessage ??= MessageStore.getMessage(channelID, messageID); if (linkedMessage) messageCache[messageID] = { message: linkedMessage, fetched: true }; @@ -224,7 +238,7 @@ var messageEmbed={mle_AutomodEmbed:$1};" } } const messageProps: MessageEmbedProps = { - message: linkedMessage, + message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]), channel: linkedChannel, guildID }; @@ -268,7 +282,7 @@ var messageEmbed={mle_AutomodEmbed:$1};" } }} renderDescription={() => { - return <div key={message.id} className={classNames.join(" ")} > + return <div key={message.id} className={classNames.join(" ")}> <ChannelMessage id={`message-link-embeds-${message.id}`} message={message} diff --git a/src/plugins/messageLogger/deleteStyleOverlay.css b/src/plugins/messageLogger/deleteStyleOverlay.css new file mode 100644 index 000000000..a05ed4da5 --- /dev/null +++ b/src/plugins/messageLogger/deleteStyleOverlay.css @@ -0,0 +1,3 @@ +.messagelogger-deleted { + background-color: rgba(240 71 71 / 15%); +} diff --git a/src/plugins/messageLogger/deleteStyleText.css b/src/plugins/messageLogger/deleteStyleText.css new file mode 100644 index 000000000..9f2d731fc --- /dev/null +++ b/src/plugins/messageLogger/deleteStyleText.css @@ -0,0 +1,3 @@ +.messagelogger-deleted div { + color: #f04747; +} diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index e650dbbf6..9242ce2f8 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -19,20 +19,23 @@ import "./messageLogger.css"; import { Settings } from "@api/settings"; +import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import Logger from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { Parser, UserStore } from "@webpack/common"; +import { moment, Parser, Timestamp, UserStore } from "@webpack/common"; -function addDeleteStyleClass() { +import overlayStyle from "./deleteStyleOverlay.css?managed"; +import textStyle from "./deleteStyleText.css?managed"; + +function addDeleteStyle() { if (Settings.plugins.MessageLogger.deleteStyle === "text") { - document.body.classList.remove("messagelogger-red-overlay"); - document.body.classList.add("messagelogger-red-text"); + enableStyle(textStyle); + disableStyle(overlayStyle); } else { - document.body.classList.remove("messagelogger-red-text"); - document.body.classList.add("messagelogger-red-overlay"); + disableStyle(textStyle); + enableStyle(overlayStyle); } } @@ -41,28 +44,21 @@ export default definePlugin({ description: "Temporarily logs deleted and edited messages.", authors: [Devs.rushii, Devs.Ven], - timestampModule: null as any, - moment: null as Function | null, - start() { - this.moment = findByPropsLazy("relativeTimeRounding", "relativeTimeThreshold"); - this.timestampModule = findByPropsLazy("messageLogger_TimestampComponent"); - - addDeleteStyleClass(); + addDeleteStyle(); }, stop() { - document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove()); - document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove()); + document.querySelectorAll(".messagelogger-deleted").forEach(e => e.remove()); + document.querySelectorAll(".messagelogger-edited").forEach(e => e.remove()); document.body.classList.remove("messagelogger-red-overlay"); document.body.classList.remove("messagelogger-red-text"); }, renderEdit(edit: { timestamp: any, content: string; }) { - const Timestamp = this.timestampModule.messageLogger_TimestampComponent; return ( <ErrorBoundary noop> - <div className="messageLogger-edited"> + <div className="messagelogger-edited"> {Parser.parse(edit.content)} <Timestamp timestamp={edit.timestamp} @@ -78,7 +74,7 @@ export default definePlugin({ makeEdit(newMessage: any, oldMessage: any): any { return { - timestamp: this.moment?.call(newMessage.edited_timestamp), + timestamp: moment?.call(newMessage.edited_timestamp), content: oldMessage.content }; }, @@ -92,7 +88,7 @@ export default definePlugin({ { label: "Red text", value: "text", default: true }, { label: "Red overlay", value: "overlay" } ], - onChange: () => addDeleteStyleClass() + onChange: () => addDeleteStyle() }, ignoreBots: { type: OptionType.BOOLEAN, @@ -155,7 +151,7 @@ export default definePlugin({ replace: "MESSAGE_DELETE:function($1){" + " var cache = $2getOrCreate($1.channelId);" + - " cache = Vencord.Plugins.plugins.MessageLogger.handleDelete(cache, $1, false);" + + " cache = $self.handleDelete(cache, $1, false);" + " $2commit(cache);" + "}," }, @@ -165,7 +161,7 @@ export default definePlugin({ replace: "MESSAGE_DELETE_BULK:function($1){" + " var cache = $2getOrCreate($1.channelId);" + - " cache = Vencord.Plugins.plugins.MessageLogger.handleDelete(cache, $1, true);" + + " cache = $self.handleDelete(cache, $1, true);" + " $2commit(cache);" + "}," }, @@ -175,7 +171,7 @@ export default definePlugin({ replace: "$1" + ".update($3,m =>" + " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" + - " m.set('editHistory',[...(m.editHistory || []), Vencord.Plugins.plugins.MessageLogger.makeEdit($2.message, m)]) :" + + " m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" + " m" + ")" + ".update($3" @@ -259,8 +255,8 @@ export default definePlugin({ replace: "$1,deleted=$2.attachment?.deleted," }, { - match: /(hiddenSpoilers:\w,className:)/, - replace: "$1 (deleted ? 'messageLogger-deleted-attachment ' : '') +" + match: /\["className","attachment","inlineMedia".+?className:/, + replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +" } ] }, @@ -276,9 +272,9 @@ export default definePlugin({ replace: "var $1=$2.id,deleted=$2.message.deleted," }, { - // Append messageLogger-deleted to classNames if deleted + // Append messagelogger-deleted to classNames if deleted match: /\)\("li",\{(.+?),className:/, - replace: ")(\"li\",{$1,className:(deleted ? \"messageLogger-deleted \" : \"\")+" + replace: ")(\"li\",{$1,className:(deleted ? \"messagelogger-deleted \" : \"\")+" } ] }, @@ -291,7 +287,7 @@ export default definePlugin({ { // Render editHistory in the deepest div for message content match: /(\)\("div",\{id:.+?children:\[)/, - replace: "$1 (arguments[0].message.editHistory.length > 0 ? arguments[0].message.editHistory.map(edit => Vencord.Plugins.plugins.MessageLogger.renderEdit(edit)) : null), " + replace: "$1 (arguments[0].message.editHistory.length > 0 ? arguments[0].message.editHistory.map(edit => $self.renderEdit(edit)) : null), " } ] }, @@ -312,17 +308,6 @@ export default definePlugin({ ] }, - { - // Message "(edited)" timestamp component - // Module 23552 - find: "Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format", - replacement: { - // Re-export the timestamp component under a findable name - match: /{(\w{1,2}:\(\)=>(\w{1,2}))}/, - replace: "{$1,messageLogger_TimestampComponent:()=>$2}" - } - }, - { // Message context base menu // Module 600300 diff --git a/src/plugins/messageLogger/messageLogger.css b/src/plugins/messageLogger/messageLogger.css index 94a3e2509..f93f33088 100644 --- a/src/plugins/messageLogger/messageLogger.css +++ b/src/plugins/messageLogger/messageLogger.css @@ -1,27 +1,20 @@ -.messagelogger-red-overlay .messageLogger-deleted { - background-color: rgba(240, 71, 71, 0.15); -} -.messagelogger-red-text .messageLogger-deleted div { - color: #f04747; -} - -.messageLogger-deleted [class^="buttons"] { +.messagelogger-deleted [class^="buttons"] { display: none; } -.messageLogger-deleted-attachment { +.messagelogger-deleted-attachment { filter: grayscale(1); } -.messageLogger-deleted-attachment:hover { +.messagelogger-deleted-attachment:hover { filter: grayscale(0); transition: 250ms filter linear; } -.theme-dark .messageLogger-edited { +.theme-dark .messagelogger-edited { filter: brightness(80%); } -.theme-light .messageLogger-edited { +.theme-light .messagelogger-edited { opacity: 0.5; } diff --git a/src/plugins/noBlockedMessages.ts b/src/plugins/noBlockedMessages.ts index bd72ce5fb..6937041a8 100644 --- a/src/plugins/noBlockedMessages.ts +++ b/src/plugins/noBlockedMessages.ts @@ -43,7 +43,7 @@ export default definePlugin({ replacement: [ { match: /(?<=MESSAGE_CREATE:function\((\w)\){var \w=\w\.channelId,\w=\w\.message,\w=\w\.isPushNotification,\w=\w\.\w\.getOrCreate\(\w\));/, - replace: ";if(Vencord.Plugins.plugins.NoBlockedMessages.isBlocked(n))return;" + replace: ";if($self.isBlocked(n))return;" } ] } diff --git a/src/plugins/noReplyMention.tsx b/src/plugins/noReplyMention.tsx index 91a88d308..2e9758893 100644 --- a/src/plugins/noReplyMention.tsx +++ b/src/plugins/noReplyMention.tsx @@ -51,7 +51,7 @@ export default definePlugin({ replacement: { match: /CREATE_PENDING_REPLY:function\((.{1,2})\){/, replace: - "CREATE_PENDING_REPLY:function($1){$1.shouldMention=Vencord.Plugins.plugins.NoReplyMention.shouldMention($1);", + "CREATE_PENDING_REPLY:function($1){$1.shouldMention=$self.shouldMention($1);", }, }, ], diff --git a/src/plugins/oneko.ts b/src/plugins/oneko.ts index ef2f5d682..d95ba2bc9 100644 --- a/src/plugins/oneko.ts +++ b/src/plugins/oneko.ts @@ -26,7 +26,7 @@ export default definePlugin({ authors: [Devs.Ven, Devs.adryd], start() { - fetch("https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.js") + fetch("https://raw.githubusercontent.com/adryd325/oneko.js/5977144dce83e4d71af1de005d16e38eebeb7b72/oneko.js") .then(x => x.text()) .then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif")) .then(eval); diff --git a/src/plugins/petpet.ts b/src/plugins/petpet.ts index a8281d06d..0d9a3d099 100644 --- a/src/plugins/petpet.ts +++ b/src/plugins/petpet.ts @@ -175,8 +175,8 @@ export default definePlugin({ gif.finish(); const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" }); // Immediately after the command finishes, Discord clears all input, including pending attachments. - // Thus, setImmediate is needed to make this execute after Discord cleared the input - setImmediate(() => promptToUpload([file], cmdCtx.channel, DRAFT_TYPE)); + // Thus, setTimeout is needed to make this execute after Discord cleared the input + setTimeout(() => promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10); }, }, ] diff --git a/src/plugins/plainFolderIcon.ts b/src/plugins/plainFolderIcon.ts index 5c01ea932..4c37e1e8d 100644 --- a/src/plugins/plainFolderIcon.ts +++ b/src/plugins/plainFolderIcon.ts @@ -24,7 +24,7 @@ export default definePlugin({ description: "Doesn't show the small guild icons in folders", authors: [Devs.botato], patches: [{ - find: "().expandedFolderIconWrapper", + find: ".expandedFolderIconWrapper", replacement: [{ match: /\(\w\|\|\w\)&&(\(.{0,40}\(.{1,3}\.animated)/, replace: "$1", diff --git a/src/plugins/platformIndicators.tsx b/src/plugins/platformIndicators.tsx index b7af8fb72..0e3d61a99 100644 --- a/src/plugins/platformIndicators.tsx +++ b/src/plugins/platformIndicators.tsx @@ -55,7 +55,7 @@ const Icons = { }; type Platform = keyof typeof Icons; -const getStatusColor = findByCodeLazy("STATUS_YELLOW", "TWITCH", "STATUS_GREY"); +const getStatusColor = findByCodeLazy(".TWITCH", ".STREAMING", ".INVISIBLE"); const PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => { const tooltip = platform[0].toUpperCase() + platform.slice(1); @@ -146,9 +146,7 @@ const indicatorLocations = { description: "Inside messages", onEnable: () => addDecoration("platform-indicator", props => <ErrorBoundary noop> - <PlatformIndicator user={ - props.decorations[1]?.find(i => i.key === "new-member")?.props.message?.author - } inline /> + <PlatformIndicator user={props.message?.author} inline /> </ErrorBoundary> ), onDisable: () => removeDecoration("platform-indicator") diff --git a/src/plugins/pronoundb/index.ts b/src/plugins/pronoundb/index.ts index c8481105d..7ebe91921 100644 --- a/src/plugins/pronoundb/index.ts +++ b/src/plugins/pronoundb/index.ts @@ -38,7 +38,7 @@ export default definePlugin({ find: "showCommunicationDisabledStyles", replacement: { match: /(?<=return\s*\(0,\w{1,3}\.jsxs?\)\(.+!\w{1,3}&&)(\(0,\w{1,3}.jsxs?\)\(.+?\{.+?\}\))/, - replace: "[$1, Vencord.Plugins.plugins.PronounDB.PronounsChatComponent(e)]" + replace: "[$1, $self.PronounsChatComponent(e)]" } }, // Hijack the discord pronouns section (hidden without experiment) and add a wrapper around the text section @@ -46,7 +46,7 @@ export default definePlugin({ find: ".Messages.BOT_PROFILE_SLASH_COMMANDS", replacement: { match: /\(0,.\.jsx\)\((?<PronounComponent>.{1,2}\..),(?<pronounProps>{currentPronouns.+?:(?<fullProps>.{1,2})\.pronouns.+?})\)/, - replace: "$<fullProps>&&Vencord.Plugins.plugins.PronounDB.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)" + replace: "$<fullProps>&&$self.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)" } }, // Make pronouns experiment be enabled by default diff --git a/src/plugins/revealAllSpoilers.ts b/src/plugins/revealAllSpoilers.ts new file mode 100644 index 000000000..b508b6a0a --- /dev/null +++ b/src/plugins/revealAllSpoilers.ts @@ -0,0 +1,58 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; + +const SpoilerClasses = findByPropsLazy("spoilerText"); +const MessagesClasses = findByPropsLazy("messagesWrapper", "messages"); + +export default definePlugin({ + name: "RevealAllSpoilers", + description: "Reveal all spoilers in a message by Ctrl-clicking a spoiler, or in the chat with Ctrl+Shift-click", + authors: [Devs.whqwert], + + patches: [ + { + find: ".revealSpoiler=function", + replacement: { + match: /\.revealSpoiler=function\((.{1,2})\){/, + replace: ".revealSpoiler=function($1){$self.reveal($1);" + } + } + ], + + reveal(event: MouseEvent) { + const { ctrlKey, shiftKey, target } = event; + + if (!ctrlKey) { return; } + + const { spoilerText, hidden } = SpoilerClasses; + const { messagesWrapper } = MessagesClasses; + + const parent = shiftKey + ? document.querySelector(`div.${messagesWrapper}`) + : (target as HTMLSpanElement).parentElement; + + for (const spoiler of parent!.querySelectorAll(`span.${spoilerText}.${hidden}`)) { + (spoiler as HTMLSpanElement).click(); + } + } + +}); diff --git a/src/plugins/reverseImageSearch.tsx b/src/plugins/reverseImageSearch.tsx index a4068ccdc..4d9f040b1 100644 --- a/src/plugins/reverseImageSearch.tsx +++ b/src/plugins/reverseImageSearch.tsx @@ -25,7 +25,8 @@ const Engines = { Yandex: "https://yandex.com/images/search?rpt=imageview&url=", SauceNAO: "https://saucenao.com/search.php?url=", IQDB: "https://iqdb.org/?url=", - TinEye: "https://www.tineye.com/search?url=" + TinEye: "https://www.tineye.com/search?url=", + ImgOps: "https://imgops.com/start?url=" }; export default definePlugin({ @@ -42,17 +43,18 @@ export default definePlugin({ } }, { // pass the target to the open link menu so we can check if it's an image - find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,", - replacement: { - // url1 = url2 = props.attachment.url - // ... - // OpenLinks(url2 != null ? url2 : url1, someStuffs) - // - // the back references are needed because the code is like Z(a!=null?b:c,d), no way to match that - // otherwise - match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/, - replace: "$&,$<props>.target" - } + find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", + replacement: [ + { + match: /ariaLabel:\i\.Z\.Messages\.MESSAGE_ACTIONS_MENU_LABEL/, + replace: "$&,_vencordTarget:arguments[0].target" + }, + { + // var f = props.itemHref, .... MakeNativeMenu(null != f ? f : blah) + match: /(\i)=\i\.itemHref,.+?\(null!=\1\?\1:.{1,10}(?=\))/, + replace: "$&,arguments[0]._vencordTarget" + } + ] }], makeMenu(src: string, target: HTMLElement) { diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index 4852967a3..c62065f7e 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -32,6 +32,7 @@ export default function ReviewsView({ userId }: { userId: string; }) { fallbackValue: [], deps: [refetchCount], }); + const username = UserStore.getUser(userId)?.username ?? ""; const dirtyRefetch = () => setRefetchCount(refetchCount + 1); @@ -79,7 +80,7 @@ export default function ReviewsView({ userId }: { userId: string; }) { <textarea className={classes(Classes.textarea.replace("textarea", ""), "enter-comment")} // this produces something like '-_59yqs ...' but since no class exists with that name its fine - placeholder={"Review @" + UserStore.getUser(userId)?.username ?? ""} + placeholder={reviews?.some(r => r.senderdiscordid === UserStore.getCurrentUser().id) ? `Update review for @${username}` : `Review @${username}`} onKeyDown={onKeyPress} style={{ marginTop: "6px", diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index 744c2d620..8e8398344 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -37,7 +37,7 @@ export default definePlugin({ find: "disableBorderColor:!0", replacement: { match: /\(.{0,10}\{user:(.),setNote:.,canDM:.,.+?\}\)/, - replace: "$&,Vencord.Plugins.plugins.ReviewDB.getReviewsComponent($1)" + replace: "$&,$self.getReviewsComponent($1)" }, } ], diff --git a/src/plugins/richerCider.tsx b/src/plugins/richerCider.tsx new file mode 100644 index 000000000..08b0096e9 --- /dev/null +++ b/src/plugins/richerCider.tsx @@ -0,0 +1,67 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 OpenAsar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { Link } from "@components/Link"; +import definePlugin from "@utils/types"; +import { Forms } from "@webpack/common"; +const appIds = [ + "911790844204437504", + "886578863147192350", + "1020414178047041627", + "1032800329332445255" +]; +export default definePlugin({ + name: "richerCider", + description: "Enhances Cider (More details in info button) by adding the \"Listening to\" type prefix to the user's rich presence when an applicable ID is found.", + authors: [{ + id: 191621342473224192n, + name: "cryptofyre", + }], + patches: [ + { + find: '.displayName="LocalActivityStore"', + replacement: { + match: /LOCAL_ACTIVITY_UPDATE:function\((\i)\)\{/, + replace: "$&$self.patchActivity($1.activity);", + } + } + ], + settingsAboutComponent: () => ( + <> + <Forms.FormTitle tag="h3">Install Cider to use this Plugin</Forms.FormTitle> + <Forms.FormText> + <Link href="https://cider.sh">Follow the link to our website</Link> to get Cider up and running, and then enable the plugin. + </Forms.FormText> + <br></br> + <Forms.FormTitle tag="h3">What is Cider?</Forms.FormTitle> + <Forms.FormText> + Cider is an open-source and community oriented Apple Music client for Windows, macOS, and Linux. + </Forms.FormText> + <br></br> + <Forms.FormTitle tag="h3">Recommended Optional Plugins</Forms.FormTitle> + <Forms.FormText> + I'd recommend using TimeBarAllActivities alongside this plugin to give off a much better visual to the eye (Keep in mind this only affects your client and will not show for other users) + </Forms.FormText> + </> + ), + patchActivity(activity: any) { + if (appIds.includes(activity.application_id)) { + activity.type = 2; /* LISTENING type */ + } + }, +}); diff --git a/src/plugins/roleColorEverywhere.tsx b/src/plugins/roleColorEverywhere.tsx new file mode 100644 index 000000000..5d48da06c --- /dev/null +++ b/src/plugins/roleColorEverywhere.tsx @@ -0,0 +1,123 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { definePluginSettings } from "@api/settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common"; + +const settings = definePluginSettings({ + chatMentions: { + type: OptionType.BOOLEAN, + default: true, + description: "Show role colors in chat mentions (including in the message box)", + restartNeeded: true + }, + memberList: { + type: OptionType.BOOLEAN, + default: true, + description: "Show role colors in member list role headers", + restartNeeded: true + }, + voiceUsers: { + type: OptionType.BOOLEAN, + default: true, + description: "Show role colors in the voice chat user list", + restartNeeded: true + } +}); + +export default definePlugin({ + name: "RoleColorEverywhere", + authors: [Devs.KingFish, Devs.lewisakura], + description: "Adds the top role color anywhere possible", + patches: [ + // Chat Mentions + { + find: 'className:"mention"', + replacement: [ + { + match: /user:(\i),channelId:(\i).{0,300}?"@"\.concat\(.+?\)/, + replace: "$&,color:$self.getUserColor($1.id,{channelId:$2})" + } + ], + predicate: () => settings.store.chatMentions, + }, + // Slate + { + // taken from CommandsAPI + find: ".source,children", + replacement: [ + { + match: /function \i\((\i)\).{5,20}id.{5,20}guildId.{5,10}channelId.{100,150}hidePersonalInformation.{5,50}jsx.{5,20},{/, + replace: "$&color:$self.getUserColor($1.id,{guildId:$1.guildId})," + } + ], + predicate: () => settings.store.chatMentions, + }, + // Member List Role Names + { + find: ".memberGroupsPlaceholder", + replacement: [ + { + match: /(memo\(\(function\((\i)\).{300,500}CHANNEL_MEMBERS_A11Y_LABEL.{100,200}roleIcon.{5,20}null,).," \u2014 ",.\]/, + replace: "$1$self.roleGroupColor($2)]" + }, + ], + predicate: () => settings.store.memberList, + }, + // Voice chat users + { + find: "renderPrioritySpeaker", + replacement: [ + { + match: /renderName=function\(\).{50,75}speaking.{50,100}jsx.{5,10}{/, + replace: "$&...$self.getVoiceProps(this.props)," + } + ], + predicate: () => settings.store.voiceUsers, + } + ], + settings, + + getColor(userId: string, { channelId, guildId }: { channelId?: string; guildId?: string; }) { + if (!(guildId ??= ChannelStore.getChannel(channelId!)?.guild_id)) return null; + return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null; + }, + getUserColor(userId: string, ids: { channelId?: string; guildId?: string; }) { + const colorString = this.getColor(userId, ids); + return colorString && parseInt(colorString.slice(1), 16); + }, + roleGroupColor({ id, count, title, guildId }: { id: string; count: number; title: string; guildId: string; }) { + const guild = GuildStore.getGuild(guildId); + const role = guild?.roles[id]; + + return <span style={{ + color: role?.colorString, + fontWeight: "unset", + letterSpacing: ".05em" + }}>{title} — {count}</span>; + }, + getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) { + return { + style: { + color: this.getColor(userId, { guildId }) + } + }; + } +}); diff --git a/src/plugins/settings.tsx b/src/plugins/settings.tsx index e5afbbcc7..67d1f8de2 100644 --- a/src/plugins/settings.tsx +++ b/src/plugins/settings.tsx @@ -17,10 +17,12 @@ */ import { Settings } from "@api/settings"; +import PatchHelper from "@components/PatchHelper"; import { Devs } from "@utils/constants"; import Logger from "@utils/Logger"; import { LazyComponent } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; +import { SettingsRouter } from "@webpack/common"; import gitHash from "~git-hash"; @@ -32,10 +34,10 @@ export default definePlugin({ authors: [Devs.Ven, Devs.Megu], required: true, patches: [{ - find: "().versionHash", + find: ".versionHash", replacement: [ { - match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}\(\)\.versionHash,.+?\})\)," "/, + match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/, replace: (m, component, props) => { props = props.replace(/children:\[.+\]/, ""); return `${m},Vencord.Plugins.plugins.Settings.makeInfoElements(${component}, ${props})`; @@ -62,23 +64,68 @@ export default definePlugin({ } } }, - replace: (m, mod) => { - const updater = !IS_WEB ? '{section:"VencordUpdater",label:"Updater",element:Vencord.Plugins.plugins.Settings.tabs.updater},' : ""; - const patchHelper = IS_DEV ? '{section:"VencordPatchHelper",label:"Patch Helper",element:Vencord.Components.PatchHelper},' : ""; - return ( - `{section:${mod}.ID.HEADER,label:"Vencord"},` + - '{section:"VencordSettings",label:"Vencord",element:Vencord.Plugins.plugins.Settings.tabs.vencord},' + - '{section:"VencordPlugins",label:"Plugins",element:Vencord.Plugins.plugins.Settings.tabs.plugins},' + - '{section:"VencordThemes",label:"Themes",element:Vencord.Plugins.plugins.Settings.tabs.themes},' + - updater + - '{section:"VencordSettingsSync",label:"Backup & Restore",element:Vencord.Plugins.plugins.Settings.tabs.sync},' + - patchHelper + - `{section:${mod}.ID.DIVIDER},${m}` - ); - } + replace: "...$self.makeSettingsCategories($1),$&" } }], + makeSettingsCategories({ ID }: { ID: Record<string, unknown>; }) { + const makeOnClick = (tab: string) => () => SettingsRouter.open(tab); + + const cats = [ + { + section: ID.HEADER, + label: "Vencord" + }, { + section: "VencordSettings", + label: "Vencord", + element: () => <SettingsComponent tab="VencordSettings" />, + onClick: makeOnClick("VencordSettings") + }, { + section: "VencordPlugins", + label: "Plugins", + element: () => <SettingsComponent tab="VencordPlugins" />, + onClick: makeOnClick("VencordPlugins") + }, { + section: "VencordThemes", + label: "Themes", + element: () => <SettingsComponent tab="VencordThemes" />, + onClick: makeOnClick("VencordThemes") + } + ] as Array<{ + section: unknown, + label?: string; + element?: React.ComponentType; + onClick?(): void; + }>; + + if (!IS_WEB) + cats.push({ + section: "VencordUpdater", + label: "Updater", + element: () => <SettingsComponent tab="VencordUpdater" />, + onClick: makeOnClick("VencordUpdater") + }); + + cats.push({ + section: "VencordSettingsSync", + label: "Backup & Restore", + element: () => <SettingsComponent tab="VencordSettingsSync" />, + onClick: makeOnClick("VencordSettingsSync") + }); + + if (IS_DEV) + cats.push({ + section: "VencordPatchHelper", + label: "Patch Helper", + element: PatchHelper!, + onClick: makeOnClick("VencordPatchHelper") + }); + + cats.push({ section: ID.DIVIDER }); + + return cats; + }, + options: { settingsLocation: { type: OptionType.SELECT, diff --git a/src/plugins/shikiCodeblocks/devicon.css b/src/plugins/shikiCodeblocks/devicon.css index f5c49212d..ed1014e38 100644 --- a/src/plugins/shikiCodeblocks/devicon.css +++ b/src/plugins/shikiCodeblocks/devicon.css @@ -1 +1 @@ -@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css'); +@import url("https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css"); diff --git a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts index 50b0fc978..22954ce1a 100644 --- a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts +++ b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts @@ -16,25 +16,25 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { useSettings } from "@api/settings"; +import { PartialExcept } from "@utils/types"; import { React } from "@webpack/common"; import { shiki } from "../api/shiki"; -import { ShikiSettings } from "../types"; +import { settings as pluginSettings, ShikiSettings } from "../settings"; -export function useShikiSettings(settingKeys: (keyof ShikiSettings)[], overrides?: Record<string, any>) { - const settings = useSettings(settingKeys.map(key => `plugins.ShikiCodeblocks.${key}`)).plugins.ShikiCodeblocks as ShikiSettings; +export function useShikiSettings<F extends keyof ShikiSettings>(settingKeys: F[], overrides?: Partial<ShikiSettings>) { + const settings: Partial<ShikiSettings> = pluginSettings.use(settingKeys); const [isLoading, setLoading] = React.useState(false); - const withOverrides = { ...settings, ...overrides }; + const withOverrides = { ...settings, ...overrides } as PartialExcept<ShikiSettings, F>; const themeUrl = withOverrides.customTheme || withOverrides.theme; if (overrides) { - const willChangeTheme = shiki.currentThemeUrl && themeUrl !== shiki.currentThemeUrl; + const willChangeTheme = shiki.currentThemeUrl && themeUrl && themeUrl !== shiki.currentThemeUrl; const noOverrides = Object.keys(overrides).length === 0; if (isLoading && (!willChangeTheme || noOverrides)) setLoading(false); - if ((!isLoading && willChangeTheme)) { + if (!isLoading && willChangeTheme) { setLoading(true); shiki.setTheme(themeUrl); } diff --git a/src/plugins/shikiCodeblocks/index.ts b/src/plugins/shikiCodeblocks/index.ts index 428a2735e..58e55b4e5 100644 --- a/src/plugins/shikiCodeblocks/index.ts +++ b/src/plugins/shikiCodeblocks/index.ts @@ -18,26 +18,19 @@ import "./shiki.css"; -import { disableStyle, enableStyle } from "@api/Styles"; +import { enableStyle } from "@api/Styles"; import { Devs } from "@utils/constants"; -import { parseUrl } from "@utils/misc"; -import { wordsFromPascal, wordsToTitle } from "@utils/text"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin from "@utils/types"; import previewExampleText from "~fileContent/previewExample.tsx"; -import { Settings } from "../../Vencord"; import { shiki } from "./api/shiki"; -import { themes } from "./api/themes"; import { createHighlighter } from "./components/Highlighter"; import deviconStyle from "./devicon.css?managed"; -import { DeviconSetting, HljsSetting, ShikiSettings } from "./types"; +import { settings } from "./settings"; +import { DeviconSetting } from "./types"; import { clearStyles } from "./utils/createStyle"; -const themeNames = Object.keys(themes); - -const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings; - export default definePlugin({ name: "ShikiCodeblocks", description: "Brings vscode-style codeblocks into Discord, powered by Shiki", @@ -52,10 +45,10 @@ export default definePlugin({ }, ], start: async () => { - if (getSettings().useDevIcon !== DeviconSetting.Disabled) + if (settings.store.useDevIcon !== DeviconSetting.Disabled) enableStyle(deviconStyle); - await shiki.init(getSettings().customTheme || getSettings().theme); + await shiki.init(settings.store.customTheme || settings.store.theme); }, stop: () => { shiki.destroy(); @@ -67,90 +60,7 @@ export default definePlugin({ isPreview: true, tempSettings, }), - options: { - theme: { - type: OptionType.SELECT, - description: "Default themes", - options: themeNames.map(themeName => ({ - label: wordsToTitle(wordsFromPascal(themeName)), - value: themes[themeName], - default: themes[themeName] === themes.DarkPlus, - })), - disabled: () => !!getSettings().customTheme, - onChange: shiki.setTheme, - }, - customTheme: { - type: OptionType.STRING, - description: "A link to a custom vscode theme", - placeholder: themes.MaterialCandy, - isValid: value => { - if (!value) return true; - const url = parseUrl(value); - if (!url) return "Must be a valid URL"; - - if (!url.pathname.endsWith(".json")) return "Must be a json file"; - - return true; - }, - onChange: value => shiki.setTheme(value || getSettings().theme), - }, - tryHljs: { - type: OptionType.SELECT, - description: "Use the more lightweight default Discord highlighter and theme.", - options: [ - { - label: "Never", - value: HljsSetting.Never, - }, - { - label: "Prefer Shiki instead of Highlight.js", - value: HljsSetting.Secondary, - default: true, - }, - { - label: "Prefer Highlight.js instead of Shiki", - value: HljsSetting.Primary, - }, - { - label: "Always", - value: HljsSetting.Always, - }, - ], - }, - useDevIcon: { - type: OptionType.SELECT, - description: "How to show language icons on codeblocks", - options: [ - { - label: "Disabled", - value: DeviconSetting.Disabled, - }, - { - label: "Colorless", - value: DeviconSetting.Greyscale, - default: true, - }, - { - label: "Colored", - value: DeviconSetting.Color, - }, - ], - onChange: (newValue: DeviconSetting) => { - if (newValue === DeviconSetting.Disabled) disableStyle(deviconStyle); - else enableStyle(deviconStyle); - }, - }, - bgOpacity: { - type: OptionType.SLIDER, - description: "Background opacity", - markers: [0, 20, 40, 60, 80, 100], - default: 100, - componentProps: { - stickToMarkers: false, - onValueRender: null, // Defaults to percentage - }, - }, - }, + settings, // exports shiki, diff --git a/src/plugins/shikiCodeblocks/settings.ts b/src/plugins/shikiCodeblocks/settings.ts new file mode 100644 index 000000000..ff5afc2e7 --- /dev/null +++ b/src/plugins/shikiCodeblocks/settings.ts @@ -0,0 +1,123 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { definePluginSettings } from "@api/settings"; +import { disableStyle, enableStyle } from "@api/Styles"; +import { parseUrl } from "@utils/misc"; +import { wordsFromPascal, wordsToTitle } from "@utils/text"; +import { OptionType } from "@utils/types"; + +import { shiki } from "./api/shiki"; +import { themes } from "./api/themes"; +import deviconStyle from "./devicon.css?managed"; +import { DeviconSetting, HljsSetting } from "./types"; + +const themeNames = Object.keys(themes) as (keyof typeof themes)[]; + +export type ShikiSettings = typeof settings.store; +export const settings = definePluginSettings({ + theme: { + type: OptionType.SELECT, + description: "Default themes", + options: themeNames.map(themeName => ({ + label: wordsToTitle(wordsFromPascal(themeName)), + value: themes[themeName], + default: themes[themeName] === themes.DarkPlus, + })), + onChange: shiki.setTheme, + }, + customTheme: { + type: OptionType.STRING, + description: "A link to a custom vscode theme", + placeholder: themes.MaterialCandy, + onChange: value => { + shiki.setTheme(value || settings.store.theme); + }, + }, + tryHljs: { + type: OptionType.SELECT, + description: "Use the more lightweight default Discord highlighter and theme.", + options: [ + { + label: "Never", + value: HljsSetting.Never, + }, + { + label: "Prefer Shiki instead of Highlight.js", + value: HljsSetting.Secondary, + default: true, + }, + { + label: "Prefer Highlight.js instead of Shiki", + value: HljsSetting.Primary, + }, + { + label: "Always", + value: HljsSetting.Always, + }, + ], + }, + useDevIcon: { + type: OptionType.SELECT, + description: "How to show language icons on codeblocks", + options: [ + { + label: "Disabled", + value: DeviconSetting.Disabled, + }, + { + label: "Colorless", + value: DeviconSetting.Greyscale, + default: true, + }, + { + label: "Colored", + value: DeviconSetting.Color, + }, + ], + onChange: (newValue: DeviconSetting) => { + if (newValue === DeviconSetting.Disabled) disableStyle(deviconStyle); + else enableStyle(deviconStyle); + }, + }, + bgOpacity: { + type: OptionType.SLIDER, + description: "Background opacity", + markers: [0, 20, 40, 60, 80, 100], + default: 100, + componentProps: { + stickToMarkers: false, + onValueRender: null, // Defaults to percentage + }, + }, +}, { + theme: { + disabled() { return !!this.store.customTheme; }, + }, + customTheme: { + isValid(value) { + if (!value) return true; + const url = parseUrl(value); + if (!url) return "Must be a valid URL"; + + if (!url.pathname.endsWith(".json")) return "Must be a json file"; + + return true; + }, + } +}); diff --git a/src/plugins/shikiCodeblocks/shiki.css b/src/plugins/shikiCodeblocks/shiki.css index d71b67392..2051183fd 100644 --- a/src/plugins/shikiCodeblocks/shiki.css +++ b/src/plugins/shikiCodeblocks/shiki.css @@ -12,7 +12,6 @@ overflow-x: auto; padding: 0.5em; position: relative; - font-size: 0.875rem; line-height: 1.125rem; text-indent: 0; @@ -47,7 +46,7 @@ padding: 4px 8px; } -.shiki-btn~.shiki-btn { +.shiki-btn ~ .shiki-btn { margin-left: 4px; } @@ -57,7 +56,7 @@ .shiki-spinner-container { align-items: center; - background-color: rgba(0, 0, 0, 0.6); + background-color: rgb(0 0 0 / 60%); display: flex; position: absolute; justify-content: center; diff --git a/src/plugins/shikiCodeblocks/types.ts b/src/plugins/shikiCodeblocks/types.ts index ee5aa9e64..e724ea438 100644 --- a/src/plugins/shikiCodeblocks/types.ts +++ b/src/plugins/shikiCodeblocks/types.ts @@ -23,8 +23,6 @@ import type { IThemeRegistration, } from "@vap/shiki"; -import type { Settings } from "../../Vencord"; - /** This must be atleast a subset of the `@vap/shiki-worker` spec */ export type ShikiSpec = { setOnigasm: ({ wasm }: { wasm: string; }) => Promise<void>; @@ -64,15 +62,3 @@ export enum DeviconSetting { Greyscale = "GREYSCALE", Color = "COLOR" } - -type CommonSettings = { - [K in keyof Settings["plugins"][string]as K extends `${infer V}` ? K : never]: Settings["plugins"][string][K]; -}; - -export interface ShikiSettings extends CommonSettings { - theme: string; - customTheme: string; - tryHljs: HljsSetting; - useDevIcon: DeviconSetting; - bgOpacity: number; -} diff --git a/src/plugins/shikiCodeblocks/utils/misc.ts b/src/plugins/shikiCodeblocks/utils/misc.ts index fefe938fc..e0c526342 100644 --- a/src/plugins/shikiCodeblocks/utils/misc.ts +++ b/src/plugins/shikiCodeblocks/utils/misc.ts @@ -21,7 +21,7 @@ import { hljs } from "@webpack/common"; import { resolveLang } from "../api/languages"; import { HighlighterProps } from "../components/Highlighter"; -import { HljsSetting, ShikiSettings } from "../types"; +import { HljsSetting } from "../types"; export const cl = classNameFactory("shiki-"); @@ -30,7 +30,7 @@ export const shouldUseHljs = ({ tryHljs, }: { lang: HighlighterProps["lang"], - tryHljs: ShikiSettings["tryHljs"], + tryHljs: HljsSetting, }) => { const hljsLang = lang ? hljs?.getLanguage?.(lang) : null; const shikiLang = lang ? resolveLang(lang) : null; @@ -45,7 +45,6 @@ export const shouldUseHljs = ({ return !langName && !!hljsLang; case HljsSetting.Never: return false; + default: return false; } - - return false; }; diff --git a/src/plugins/showHiddenChannels.tsx b/src/plugins/showHiddenChannels.tsx deleted file mode 100644 index 7f5263532..000000000 --- a/src/plugins/showHiddenChannels.tsx +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - - -import { Settings } from "@api/settings"; -import { Flex } from "@components/Flex"; -import { Devs } from "@utils/constants"; -import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import definePlugin, { OptionType } from "@utils/types"; -import { waitFor } from "@webpack"; -import { Button, ChannelStore, SnowflakeUtils, Text } from "@webpack/common"; - -const CONNECT = 1048576n; -const VIEW_CHANNEL = 1024n; - -let can = (permission, channel) => true; -waitFor(m => m.can && m.initialize, m => ({ can } = m)); - -export default definePlugin({ - name: "ShowHiddenChannels", - description: "Show hidden channels", - authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX], - options: { - hideUnreads: { - description: "Hide unreads", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true // Restart is needed to refresh channel list - } - }, - patches: [ - { - // RenderLevel defines if a channel is hidden, collapsed in category, visible, etc - find: ".CannotShow", - replacement: { - match: /renderLevel:(\w+)\.CannotShow/g, - replace: "renderLevel:Vencord.Plugins.plugins.ShowHiddenChannels.shouldShow(this.record, this.category, this.isMuted)?$1.Show:$1.CannotShow" - } - }, - { - // This is where the logic that chooses the icon is, we override it to be a locked voice channel if it's hidden - find: ".rulesChannelId))", - replacement: { - match: /(\w+)\.locked(.*?)switch\((\w+)\.type\)({case \w+\.\w+\.GUILD_ANNOUNCEMENT)/g, - replace: "Vencord.Plugins.plugins.ShowHiddenChannels.isHiddenChannel($3)||$1.locked$2switch($3._isHiddenChannel?2:$3.type)$4" - } - }, - { - // inside the onMouseClick handler, we check if the channel is hidden and open the modal if it is - find: ".handleThreadsPopoutClose();", - replacement: { - match: /((\w)\.handleThreadsPopoutClose\(\);)/g, - replace: "if(arguments[0].button===0&&Vencord.Plugins.plugins.ShowHiddenChannels.channelSelected($2?.props?.channel))return;$1" - } - }, - { - // Prevent categories from disappearing when they're collapsed - find: ".prototype.shouldShowEmptyCategory=function(){", - replacement: { - match: /(\.prototype\.shouldShowEmptyCategory=function\(\){)/g, - replace: "$1return true;" - } - }, - { - // Hide unreads - find: "?\"button\":\"link\"", - predicate: () => Settings.plugins.ShowHiddenChannels.hideUnreads === true, - replacement: { - match: /(\w)\.connected,(\w)=(\w\.unread),(\w=\w\.canHaveDot)/g, - replace: "$1.connected,$2=Vencord.Plugins.plugins.ShowHiddenChannels.isHiddenChannel($1.channel)?false:$3,$4" - } - }, - { - // Hide New unreads box for hidden channels - find: '.displayName="ChannelListUnreadsStore"', - replacement: { - match: /((.)\.getGuildId\(\))(&&\(!\(.\.isThread.{1,100}\.hasRelevantUnread\()/, - replace: "$1&&!$2._isHiddenChannel$3" - } - } - ], - shouldShow(channel, category, isMuted) { - if (!this.isHiddenChannel(channel)) return false; - if (!category) return false; - if (channel.type === 0 && category.guild?.hideMutedChannels && isMuted) return false; - - return !category.isCollapsed; - }, - isHiddenChannel(channel) { - if (!channel) return false; - if (channel.channelId) - channel = ChannelStore.getChannel(channel.channelId); - if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) - return false; - - // check for disallowed voice channels too so that they get hidden when collapsing the category - channel._isHiddenChannel = !can(VIEW_CHANNEL, channel) || (channel.type === 2 && !can(CONNECT, channel)); - return channel._isHiddenChannel; - }, - channelSelected(channel) { - if (!channel) return false; - const isHidden = this.isHiddenChannel(channel); - // check for type again, otherwise it would show it for hidden stage channels - if (channel.type === 0 && isHidden) { - const lastMessageDate = channel.lastMessageId ? new Date(SnowflakeUtils.extractTimestamp(channel.lastMessageId)).toLocaleString() : null; - openModal(modalProps => ( - <ModalRoot size={ModalSize.SMALL} {...modalProps}> - <ModalHeader> - <Flex> - <Text variant="heading-md/bold">{channel.name}</Text> - {(channel.isNSFW() && ( - <Text style={{ backgroundColor: "var(--status-danger)", borderRadius: "8px", paddingLeft: 4, paddingRight: 4 }} variant="heading-md/normal"> - NSFW - </Text> - ))} - </Flex> - </ModalHeader> - <ModalContent style={{ marginBottom: 10, marginTop: 10, marginRight: 8, marginLeft: 8 }}> - <Text variant="text-md/normal">You don't have the permission to view the messages in this channel.</Text> - {(channel.topic || "").length > 0 && ( - <> - <Text variant="text-md/bold" style={{ marginTop: 10 }}> - Topic: - </Text> - <Text variant="code">{channel.topic}</Text> - </> - )} - {lastMessageDate && ( - <> - <Text variant="text-md/bold" style={{ marginTop: 10 }}> - Last message sent: - </Text> - <Text variant="code">{lastMessageDate}</Text> - </> - )} - </ModalContent> - <ModalFooter> - <Flex> - <Button - onClick={modalProps.onClose} - size={Button.Sizes.SMALL} - color={Button.Colors.PRIMARY} - > - Close - </Button> - </Flex> - </ModalFooter> - </ModalRoot> - )); - } - return isHidden; - } -}); diff --git a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx new file mode 100644 index 000000000..7e66a6a1c --- /dev/null +++ b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx @@ -0,0 +1,263 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { LazyComponent } from "@utils/misc"; +import { formatDuration } from "@utils/text"; +import { find, findByCode, findByPropsLazy } from "@webpack"; +import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common"; +import { Channel } from "discord-types/general"; + +enum SortOrderTypes { + LATEST_ACTIVITY = 0, + CREATION_DATE = 1 +} + +enum ForumLayoutTypes { + DEFAULT = 0, + LIST = 1, + GRID = 2 +} + +interface DefaultReaction { + emojiId: string | null; + emojiName: string | null; +} + +interface Tag { + id: string; + name: string; + emojiId: string | null; + emojiName: string | null; + moderated: boolean; +} + +interface ExtendedChannel extends Channel { + defaultThreadRateLimitPerUser?: number; + defaultSortOrder?: SortOrderTypes | null; + defaultForumLayout?: ForumLayoutTypes; + defaultReactionEmoji?: DefaultReaction | null; + availableTags?: Array<Tag>; +} + +enum ChannelTypes { + GUILD_TEXT = 0, + GUILD_VOICE = 2, + GUILD_ANNOUNCEMENT = 5, + GUILD_STAGE_VOICE = 13, + GUILD_FORUM = 15 +} + +enum VideoQualityModes { + AUTO = 1, + FULL = 2 +} + +enum ChannelFlags { + PINNED = 1 << 1, + REQUIRE_TAG = 1 << 4 +} + +const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase"); +const TagComponent = LazyComponent(() => find(m => { + if (typeof m !== "function") return false; + + const code = Function.prototype.toString.call(m); + // Get the component which doesn't include increasedActivity logic + return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill"); +})); +const EmojiComponent = LazyComponent(() => findByCode('.jumboable?"jumbo":"default"')); +// The component for the beggining of a channel, but we patched it so it only returns the allowed users and roles components for hidden channels +const ChannelBeginHeader = LazyComponent(() => findByCode(".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE")); + +const ChannelTypesToChannelNames = { + [ChannelTypes.GUILD_TEXT]: "text", + [ChannelTypes.GUILD_ANNOUNCEMENT]: "announcement", + [ChannelTypes.GUILD_FORUM]: "forum", + [ChannelTypes.GUILD_VOICE]: "voice", + [ChannelTypes.GUILD_STAGE_VOICE]: "stage" +}; + +const SortOrderTypesToNames = { + [SortOrderTypes.LATEST_ACTIVITY]: "Latest activity", + [SortOrderTypes.CREATION_DATE]: "Creation date" +}; + +const ForumLayoutTypesToNames = { + [ForumLayoutTypes.DEFAULT]: "Not set", + [ForumLayoutTypes.LIST]: "List view", + [ForumLayoutTypes.GRID]: "Gallery view" +}; + +const VideoQualityModesToNames = { + [VideoQualityModes.AUTO]: "Automatic", + [VideoQualityModes.FULL]: "720p" +}; + +// Icon from the modal when clicking a message link you don't have access to view +const HiddenChannelLogo = "/assets/433e3ec4319a9d11b0cbe39342614982.svg"; + +function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { + const { + type, + topic, + lastMessageId, + defaultForumLayout, + lastPinTimestamp, + defaultAutoArchiveDuration, + availableTags, + id: channelId, + rateLimitPerUser, + defaultThreadRateLimitPerUser, + defaultSortOrder, + defaultReactionEmoji, + bitrate, + rtcRegion, + videoQualityMode, + permissionOverwrites + } = channel; + + const membersToFetch: Array<string> = []; + + const guildOwnerId = GuildStore.getGuild(channel.guild_id).ownerId; + if (!GuildMemberStore.getMember(channel.guild_id, guildOwnerId)) membersToFetch.push(guildOwnerId); + + Object.values(permissionOverwrites).forEach(({ type, id: userId }) => { + if (type === 1) { + if (!GuildMemberStore.getMember(channel.guild_id, userId)) membersToFetch.push(userId); + } + }); + + if (membersToFetch.length > 0) { + FluxDispatcher.dispatch({ + type: "GUILD_MEMBERS_REQUEST", + guildIds: [channel.guild_id], + userIds: membersToFetch + }); + } + + return ( + <div className={ChatScrollClasses.auto + " " + "shc-lock-screen-outer-container"}> + <div className="shc-lock-screen-container"> + <img className="shc-lock-screen-logo" src={HiddenChannelLogo} /> + + <div className="shc-lock-screen-heading-container"> + <Text variant="heading-xxl/bold">This is a hidden {ChannelTypesToChannelNames[type]} channel.</Text> + {channel.isNSFW() && + <Tooltip text="NSFW"> + {({ onMouseLeave, onMouseEnter }) => ( + <svg + onMouseLeave={onMouseLeave} + onMouseEnter={onMouseEnter} + className="shc-lock-screen-heading-nsfw-icon" + width="32" + height="32" + viewBox="0 0 48 48" + aria-hidden={true} + role="img" + > + <path d="M.7 43.05 24 2.85l23.3 40.2Zm23.55-6.25q.75 0 1.275-.525.525-.525.525-1.275 0-.75-.525-1.3t-1.275-.55q-.8 0-1.325.55-.525.55-.525 1.3t.55 1.275q.55.525 1.3.525Zm-1.85-6.1h3.65V19.4H22.4Z" /> + </svg> + )} + </Tooltip> + } + </div> + + {(!channel.isGuildVoice() && !channel.isGuildStageVoice()) && ( + <Text variant="text-lg/normal"> + You can not see the {channel.isForumChannel() ? "posts" : "messages"} of this channel. + {channel.isForumChannel() && topic && topic.length > 0 && "However you may see its guidelines:"} + </Text > + )} + + {channel.isForumChannel() && topic && topic.length > 0 && ( + <div className="shc-lock-screen-topic-container"> + {Parser.parseTopic(topic, false, { channelId })} + </div> + )} + + {lastMessageId && + <Text variant="text-md/normal"> + Last {channel.isForumChannel() ? "post" : "message"} created: + <Timestamp timestamp={moment(SnowflakeUtils.extractTimestamp(lastMessageId))} /> + </Text> + } + + {lastPinTimestamp && + <Text variant="text-md/normal">Last message pin: <Timestamp timestamp={moment(lastPinTimestamp)} /></Text> + } + {(rateLimitPerUser ?? 0) > 0 && + <Text variant="text-md/normal">Slowmode: {formatDuration(rateLimitPerUser!, "seconds")}</Text> + } + {(defaultThreadRateLimitPerUser ?? 0) > 0 && + <Text variant="text-md/normal"> + Default thread slowmode: {formatDuration(defaultThreadRateLimitPerUser!, "seconds")} + </Text> + } + {((channel.isGuildVoice() || channel.isGuildStageVoice()) && bitrate != null) && + <Text variant="text-md/normal">Bitrate: {bitrate} bits</Text> + } + {rtcRegion !== undefined && + <Text variant="text-md/normal">Region: {rtcRegion ?? "Automatic"}</Text> + } + {(channel.isGuildVoice() || channel.isGuildStageVoice()) && + <Text variant="text-md/normal">Video quality mode: {VideoQualityModesToNames[videoQualityMode ?? VideoQualityModes.AUTO]}</Text> + } + {(defaultAutoArchiveDuration ?? 0) > 0 && + <Text variant="text-md/normal"> + Default inactivity duration before archiving {channel.isForumChannel() ? "posts" : "threads"}: + {" " + formatDuration(defaultAutoArchiveDuration!, "minutes")} + </Text> + } + {defaultForumLayout != null && + <Text variant="text-md/normal">Default layout: {ForumLayoutTypesToNames[defaultForumLayout]}</Text> + } + {defaultSortOrder != null && + <Text variant="text-md/normal">Default sort order: {SortOrderTypesToNames[defaultSortOrder]}</Text> + } + {defaultReactionEmoji != null && + <div className="shc-lock-screen-default-emoji-container"> + <Text variant="text-md/normal">Default reaction emoji:</Text> + <EmojiComponent node={{ + type: defaultReactionEmoji.emojiName ? "emoji" : "customEmoji", + name: defaultReactionEmoji.emojiName ?? "", + emojiId: defaultReactionEmoji.emojiId + }} /> + </div> + } + {channel.hasFlag(ChannelFlags.REQUIRE_TAG) && + <Text variant="text-md/normal">Posts on this forum require a tag to be set.</Text> + } + {availableTags && availableTags.length > 0 && + <div className="shc-lock-screen-tags-container"> + <Text variant="text-lg/bold">Available tags:</Text> + <div className="shc-lock-screen-tags"> + {availableTags.map(tag => <TagComponent tag={tag} />)} + </div> + </div> + } + <div className="shc-lock-screen-allowed-users-and-roles-container"> + <Text variant="text-lg/bold">Allowed users and roles:</Text> + <ChannelBeginHeader channel={channel} /> + </div> + </div> + </div> + ); +} + +export default ErrorBoundary.wrap(HiddenChannelLockScreen); diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx new file mode 100644 index 000000000..fe14fe68b --- /dev/null +++ b/src/plugins/showHiddenChannels/index.tsx @@ -0,0 +1,370 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import "./style.css"; + +import { definePluginSettings } from "@api/settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common"; +import { Channel } from "discord-types/general"; + +import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen"; + +const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer"); + +const VIEW_CHANNEL = 1n << 10n; + +enum ShowMode { + LockIcon, + HiddenIconWithMutedStyle +} + +const settings = definePluginSettings({ + hideUnreads: { + description: "Hide Unreads", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + }, + showMode: { + description: "The mode used to display hidden channels.", + type: OptionType.SELECT, + options: [ + { label: "Plain style with Lock Icon instead", value: ShowMode.LockIcon, default: true }, + { label: "Muted style with hidden eye icon on the right", value: ShowMode.HiddenIconWithMutedStyle }, + ], + restartNeeded: true + } +}); + +export default definePlugin({ + name: "ShowHiddenChannels", + description: "Show channels that you do not have access to view.", + authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.dzshn], + settings, + + patches: [ + { + // RenderLevel defines if a channel is hidden, collapsed in category, visible, etc + find: ".CannotShow", + // These replacements only change the necessary CannotShow's + replacement: [ + { + match: /(?<=isChannelGatedAndVisible\(this\.record\.guild_id,this\.record\.id\).+?renderLevel:)(?<RenderLevels>\i)\..+?(?=,)/, + replace: "this.category.isCollapsed?$<RenderLevels>.WouldShowIfUncollapsed:$<RenderLevels>.Show" + }, + // Move isChannelGatedAndVisible renderLevel logic to the bottom to not show hidden channels in case they are muted + { + match: /(?<=(?<permissionCheck>if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{)if\(this\.id===\i\).+?};)(?<isChannelGatedAndVisibleCondition>if\(!\i\.\i\.isChannelGatedAndVisible\(.+?})(?<restOfFunction>.+?)(?=return{renderLevel:\i\.Show.{1,40}return \i)/, + replace: "$<restOfFunction>$<permissionCheck>$<isChannelGatedAndVisibleCondition>}" + }, + { + match: /(?<=renderLevel:(?<renderLevelExpression>\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/, + replace: "$<renderLevelExpression>" + }, + { + match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(?<RenderLevels>\i)\..+?(?=,)/, + replace: "$<RenderLevels>.Show" + }, + { + match: /(?<=getRenderLevel=function.+?return ).+?\?(?<renderLevelExpressionWithoutPermCheck>.+?):\i\.CannotShow(?=})/, + replace: "$<renderLevelExpressionWithoutPermCheck>" + } + ] + }, + { + find: "VoiceChannel, transitionTo: Channel does not have a guildId", + replacement: [ + { + // Do not show confirmation to join a voice channel when already connected to another if clicking on a hidden voice channel + match: /(?<=getCurrentClientVoiceChannelId\(\i\.guild_id\);if\()(?=.+?\((?<channel>\i)\))/, + replace: "!$self.isHiddenChannel($<channel>)&&" + }, + { + // Make Discord think we are connected to a voice channel so it shows us inside it + match: /(?=\|\|\i\.default\.selectVoiceChannel\((?<channel>\i)\.id\))/, + replace: "||$self.isHiddenChannel($<channel>)" + }, + { + // Make Discord think we are connected to a voice channel so it shows us inside it + match: /(?<=\|\|\i\.default\.selectVoiceChannel\((?<channel>\i)\.id\);!__OVERLAY__&&\()/, + replace: "$self.isHiddenChannel($<channel>)||" + } + ] + }, + { + find: "VoiceChannel.renderPopout: There must always be something to render", + replacement: [ + // Render null instead of the buttons if the channel is hidden + ...[ + "renderEditButton", + "renderInviteButton", + "renderOpenChatButton" + ].map(func => ({ + match: new RegExp(`(?<=\\i\\.${func}=function\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions + replace: "if($self.isHiddenChannel(this.props.channel))return null;" + })) + ] + }, + { + find: ".Messages.CHANNEL_TOOLTIP_DIRECTORY", + predicate: () => settings.store.showMode === ShowMode.LockIcon, + replacement: { + // Lock Icon + match: /(?=switch\((?<channel>\i)\.type\).{1,30}\.GUILD_ANNOUNCEMENT.{1,30}\(0,\i\.\i\))/, + replace: "if($self.isHiddenChannel($<channel>))return $self.LockIcon;" + } + }, + { + find: ".UNREAD_HIGHLIGHT", + predicate: () => settings.store.hideUnreads === true, + replacement: { + // Hide unreads + match: /(?<=\i\.connected,\i=)(?=(?<props>\i)\.unread)/, + replace: "$self.isHiddenChannel($<props>.channel)?false:" + } + }, + { + find: ".UNREAD_HIGHLIGHT", + predicate: () => settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, + replacement: [ + // Make the channel appear as muted if it's hidden + { + match: /(?<=\i\.name,\i=)(?=(?<props>\i)\.muted)/, + replace: "$self.isHiddenChannel($<props>.channel)?true:" + }, + // Add the hidden eye icon if the channel is hidden + { + match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/, + replace: ",$self.isHiddenChannel($<channel>)?$self.HiddenChannelIcon():null" + }, + // Make voice channels also appear as muted if they are muted + { + match: /(?<=\i\(\)\.wrapper:\i\(\)\.notInteractive,)(?<otherClasses>.+?)(?<mutedClassExpression>(?<isMuted>\i)\?\i\.MUTED)/, + replace: "$<mutedClassExpression>:\"\",$<otherClasses>$<isMuted>?\"\"" + } + ] + }, + // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden + { + find: ".UNREAD_HIGHLIGHT", + predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, + replacement: { + match: /(?<=(?<channel>\i)=\i\.channel,.+?\.LOCKED:\i)/, + replace: "&&!($self.settings.store.hideUnreads===false&&$self.isHiddenChannel($<channel>))" + } + }, + { + // Hide New unreads box for hidden channels + find: '.displayName="ChannelListUnreadsStore"', + replacement: { + match: /(?<=return null!=(?<channel>\i))(?=.{1,130}hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module + replace: "&&!$self.isHiddenChannel($<channel>)" + } + }, + // Only render the channel header and buttons that work when transitioning to a hidden channel + { + find: "Missing channel in Channel.renderHeaderToolbar", + replacement: [ + { + match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_TEXT:)(?=.+?;(?<pushNotificationButtonExpression>.+?{channel:(?<channel>\i)},"notifications"\)\);))/, + replace: "if($self.isHiddenChannel($<channel>)){$<pushNotificationButtonExpression>break;}" + }, + { + match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(?<pushNotificationButtonExpression>.+?{channel:(?<channel>\i)},"notifications"\)\)))/, + replace: "if($self.isHiddenChannel($<channel>)){$<pushNotificationButtonExpression>;break;}" + }, + { + match: /(?<=(?<this>\i)\.renderMobileToolbar=function.+?case \i\.\i\.GUILD_FORUM:)/, + replace: "if($self.isHiddenChannel($<this>.props.channel))break;" + }, + { + match: /(?<=renderHeaderBar=function.+?hideSearch:(?<channel>\i)\.isDirectory\(\))/, + replace: "||$self.isHiddenChannel($<channel>)" + }, + { + match: /(?<=renderSidebar=function\(\){)/, + replace: "if($self.isHiddenChannel(this.props.channel))return null;" + }, + { + match: /(?<=renderChat=function\(\){)/, + replace: "if($self.isHiddenChannel(this.props.channel))return $self.HiddenChannelLockScreen(this.props.channel);" + } + ] + }, + // Avoid trying to fetch messages from hidden channels + { + find: '"MessageManager"', + replacement: [ + { + match: /(?<=if\(null!=(?<channelId>\i)\).{1,100}"Skipping fetch because channelId is a static route".{1,10}else{)/, + replace: "if($self.isHiddenChannel({channelId:$<channelId>}))return;" + }, + ] + }, + // Patch keybind handlers so you can't accidentally jump to hidden channels + { + find: '"alt+shift+down"', + replacement: { + match: /(?<=getChannel\(\i\);return null!=(?<channel>\i))(?=.{1,130}hasRelevantUnread\(\i\))/, + replace: "&&!$self.isHiddenChannel($<channel>)" + } + }, + { + find: '"alt+down"', + replacement: { + match: /(?<=getState\(\)\.channelId.{1,30}\(0,\i\.\i\)\(\i\))(?=\.map\()/, + replace: ".filter(ch=>!$self.isHiddenChannel(ch))" + } + }, + // Export the emoji component used on the lock screen + { + find: 'jumboable?"jumbo":"default"', + replacement: { + match: /(?<=\i:\(\)=>\i)(?=}.+?(?<component>\i)=function.{1,20}node,\i=\i.isInteracting)/, + replace: ",hc1:()=>$<component>" // Blame Ven length check for the small name :pensive_cry: + } + }, + { + find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE", + replacement: [ + { + // Export the channel beggining header + match: /(?<=\i:\(\)=>\i)(?=}.+?function (?<component>\i).{1,600}computePermissionsForRoles)/, + replace: ",hc2:()=>$<component>" + }, + { + // Patch the header to only return allowed users and roles if it's a hidden channel (Like when it's used on the HiddenChannelLockScreen) + match: /(?<=MANAGE_ROLES.{1,60}return)(?=\(.+?(?<component>\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(?<channel>\i)\.guild_id.+?roleColor.+?]}\)))/, + replace: " $self.isHiddenChannel($<channel>)?$<component>:" + } + ] + }, + { + find: ".Messages.SHOW_CHAT", + replacement: [ + { + // Remove the divider and the open chat button for the HiddenChannelLockScreen + match: /(?<=function \i\((?<props>\i)\).{1,2000}"more-options-popout"\)\);if\()/, + replace: "(!$self.isHiddenChannel($<props>.channel)||$<props>.inCall)&&" + }, + { + // Render our HiddenChannelLockScreen component instead of the main voice channel component + match: /(?<=renderContent=function.{1,1700}children:)/, + replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?$self.HiddenChannelLockScreen(this.props.channel):" + }, + { + // Disable gradients for the HiddenChannelLockScreen of voice channels + match: /(?<=renderContent=function.{1,1600}disableGradients:)/, + replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)||" + }, + { + // Disable useless components for the HiddenChannelLockScreen of voice channels + match: /(?<=renderContent=function.{1,800}render(?!Header).{0,30}:)(?!void)/g, + replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?null:" + } + ] + }, + { + find: "Guild voice channel without guild id.", + replacement: [ + { + // Render our HiddenChannelLockScreen component instead of the main stage channel component + match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1400}children:)(?=.{1,20}}\)}function)/, + replace: "$self.isHiddenChannel($<channel>)?$self.HiddenChannelLockScreen($<channel>):" + }, + { + // Disable useless components for the HiddenChannelLockScreen of stage channels + match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1000}render(?!Header).{0,30}:)/g, + replace: "$self.isHiddenChannel($<channel>)?null:" + }, + // Prevent Discord from replacing our route if we aren't connected to the stage channel + { + match: /(?<=if\()(?=!\i&&!\i&&!\i.{1,80}(?<channel>\i)\.getGuildId\(\).{1,50}Guild voice channel without guild id\.)/, + replace: "!$self.isHiddenChannel($<channel>)&&" + }, + { + // Disable gradients for the HiddenChannelLockScreen of stage channels + match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}disableGradients:)/, + replace: "$self.isHiddenChannel($<channel>)||" + }, + { + // Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels + match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}style:)/, + replace: "$self.isHiddenChannel($<channel>)?undefined:" + }, + { + // Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen + match: /\(0,\i\.jsx\)\(\i\.\i\.Divider.+?}\)]}\)(?=.+?:(?<channel>\i)\.guild_id)/, + replace: "$self.isHiddenChannel($<channel>)?null:($&)" + }, + { + // Remove the open chat button for the HiddenChannelLockScreen + match: /(?<=null,)(?=.{1,120}channelId:(?<channel>\i)\.id,.+?toggleRequestToSpeakSidebar:\i,iconClassName:\i\(\)\.buttonIcon)/, + replace: "!$self.isHiddenChannel($<channel>)&&" + } + ], + } + ], + + isHiddenChannel(channel: Channel & { channelId?: string; }) { + if (!channel) return false; + + if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId); + if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false; + + return !PermissionStore.can(VIEW_CHANNEL, channel); + }, + + HiddenChannelLockScreen: (channel: any) => <HiddenChannelLockScreen channel={channel} />, + + LockIcon: () => ( + <svg + className={ChannelListClasses.icon} + height="18" + width="20" + viewBox="0 0 24 24" + aria-hidden={true} + role="img" + > + <path className="shc-evenodd-fill-current-color" d="M17 11V7C17 4.243 14.756 2 12 2C9.242 2 7 4.243 7 7V11C5.897 11 5 11.896 5 13V20C5 21.103 5.897 22 7 22H17C18.103 22 19 21.103 19 20V13C19 11.896 18.103 11 17 11ZM12 18C11.172 18 10.5 17.328 10.5 16.5C10.5 15.672 11.172 15 12 15C12.828 15 13.5 15.672 13.5 16.5C13.5 17.328 12.828 18 12 18ZM15 11H9V7C9 5.346 10.346 4 12 4C13.654 4 15 5.346 15 7V11Z" /> + </svg> + ), + + HiddenChannelIcon: ErrorBoundary.wrap(() => ( + <Tooltip text="Hidden Channel"> + {({ onMouseLeave, onMouseEnter }) => ( + <svg + onMouseLeave={onMouseLeave} + onMouseEnter={onMouseEnter} + className={ChannelListClasses.icon + " " + "shc-hidden-channel-icon"} + width="24" + height="24" + viewBox="0 0 24 24" + aria-hidden={true} + role="img" + > + <path className="shc-evenodd-fill-current-color" d="m19.8 22.6-4.2-4.15q-.875.275-1.762.413Q12.95 19 12 19q-3.775 0-6.725-2.087Q2.325 14.825 1 11.5q.525-1.325 1.325-2.463Q3.125 7.9 4.15 7L1.4 4.2l1.4-1.4 18.4 18.4ZM12 16q.275 0 .512-.025.238-.025.513-.1l-5.4-5.4q-.075.275-.1.513-.025.237-.025.512 0 1.875 1.312 3.188Q10.125 16 12 16Zm7.3.45-3.175-3.15q.175-.425.275-.862.1-.438.1-.938 0-1.875-1.312-3.188Q13.875 7 12 7q-.5 0-.938.1-.437.1-.862.3L7.65 4.85q1.025-.425 2.1-.638Q10.825 4 12 4q3.775 0 6.725 2.087Q21.675 8.175 23 11.5q-.575 1.475-1.512 2.738Q20.55 15.5 19.3 16.45Zm-4.625-4.6-3-3q.7-.125 1.288.112.587.238 1.012.688.425.45.613 1.038.187.587.087 1.162Z" /> + </svg> + )} + </Tooltip> + ), { noop: true }) +}); diff --git a/src/plugins/showHiddenChannels/style.css b/src/plugins/showHiddenChannels/style.css new file mode 100644 index 000000000..0f85b5084 --- /dev/null +++ b/src/plugins/showHiddenChannels/style.css @@ -0,0 +1,106 @@ +.shc-lock-screen-outer-container { + background-color: var(--background-primary); + overflow: hidden scroll; + flex: 1 1 auto; + height: 100%; + width: 100%; +} + +.shc-lock-screen-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + min-height: 100%; +} + +.shc-lock-screen-container > * { + margin: 5px; +} + +.shc-lock-screen-logo { + width: 180px; + height: 180px; +} + +.shc-lock-screen-heading-container { + display: flex; + flex-direction: row; + align-items: center; +} + +.shc-lock-screen-heading-container > * { + margin: inherit; +} + +.shc-lock-screen-heading-nsfw-icon > path { + fill: var(--text-normal); + fill-rule: evenodd; +} + +.shc-lock-screen-topic-container { + color: var(--text-normal); + background-color: var(--background-secondary); + border-radius: 5px; + padding: 10px; + max-width: 70vw; +} + +.shc-lock-screen-tags-container { + background-color: var(--background-secondary); + border-radius: 5px; + padding: 10px; + max-width: 70vw; +} + +.shc-lock-screen-tags-container > * { + margin: inherit; +} + +.shc-lock-screen-tags { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 8px; +} + +.shc-evenodd-fill-current-color { + fill-rule: evenodd; + fill: currentcolor; +} + +.shc-hidden-channel-icon { + margin-left: 6px; + z-index: 0; + cursor: not-allowed; +} + +.shc-lock-screen-default-emoji-container { + display: flex; + flex-direction: row; + align-items: center; +} + +.shc-lock-screen-default-emoji-container > [class^="emojiContainer"] { + background-color: var(--background-secondary); + border-radius: 8px; + padding: 3px 4px; + margin-left: 5px; +} + +.shc-lock-screen-allowed-users-and-roles-container { + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--background-secondary); + border-radius: 5px; + padding: 10px; + max-width: 70vw; +} + +.shc-lock-screen-allowed-users-and-roles-container > [class^="members"] { + margin-left: 10px; + flex-wrap: wrap; +} diff --git a/src/plugins/silentTyping.tsx b/src/plugins/silentTyping.tsx new file mode 100644 index 000000000..78bb92ea0 --- /dev/null +++ b/src/plugins/silentTyping.tsx @@ -0,0 +1,118 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; +import { definePluginSettings } from "@api/settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, React, Tooltip } from "@webpack/common"; + +const settings = definePluginSettings({ + showIcon: { + type: OptionType.BOOLEAN, + default: false, + description: "Show an icon for toggling the plugin", + restartNeeded: true, + }, + isEnabled: { + type: OptionType.BOOLEAN, + description: "Toggle functionality", + default: true, + } +}); + +function SilentTypingToggle() { + const { isEnabled } = settings.use(["isEnabled"]); + const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; + + return ( + <Tooltip text={isEnabled ? "Disable silent typing" : "Enable silent typing"}> + {(tooltipProps: any) => ( + <div style={{ display: "flex" }}> + <Button + {...tooltipProps} + onClick={toggle} + size="" + look={ButtonLooks.BLANK} + innerClassName={ButtonWrapperClasses.button} + style={{ margin: "0 8px 0" }} + > + <div className={ButtonWrapperClasses.buttonWrapper}> + <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"> + <path fill="currentColor" d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" /> + {isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" stroke-width="72" stroke-linecap="round" />} + </svg> + </div> + </Button> + </div> + )} + </Tooltip> + ); +} + +export default definePlugin({ + name: "SilentTyping", + authors: [Devs.Ven, Devs.dzshn], + description: "Hide that you are typing", + patches: [ + { + find: "startTyping:", + replacement: { + match: /startTyping:.+?,stop/, + replace: "startTyping:$self.startTyping,stop" + } + }, + { + find: ".activeCommandOption", + predicate: () => settings.store.showIcon, + replacement: { + match: /\i=\i\.activeCommand,\i=\i\.activeCommandOption,.{1,133}(.)=\[\];/, + replace: "$&;$1.push($self.chatBarIcon());", + } + }, + ], + dependencies: ["CommandsAPI"], + settings, + commands: [{ + name: "silenttype", + description: "Toggle whether you're hiding that you're typing or not.", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [ + { + name: "value", + description: "whether to hide or not that you're typing (default is toggle)", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + ], + execute: async (args, ctx) => { + settings.store.isEnabled = !!findOption(args, "value", !settings.store.isEnabled); + sendBotMessage(ctx.channel.id, { + content: settings.store.isEnabled ? "Silent typing enabled!" : "Silent typing disabled!", + }); + }, + }], + + async startTyping(channelId: string) { + if (settings.store.isEnabled) return; + FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); + }, + + chatBarIcon: ErrorBoundary.wrap(SilentTypingToggle, { noop: true }), +}); diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index f6ad08b83..439ecc249 100644 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ b/src/plugins/spotifyControls/PlayerComponent.tsx @@ -23,8 +23,8 @@ import { Flex } from "@components/Flex"; import { Link } from "@components/Link"; import { debounce } from "@utils/debounce"; import { classes, LazyComponent } from "@utils/misc"; -import { filters, find, findByCodeLazy } from "@webpack"; -import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState } from "@webpack/common"; +import { filters, find } from "@webpack"; +import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; import { SpotifyStore, Track } from "./SpotifyStore"; @@ -37,14 +37,6 @@ function msToHuman(ms: number) { return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; } -const useStateFromStores: <T>( - stores: typeof SpotifyStore[], - mapper: () => T, - idk?: null, - compare?: (old: T, newer: T) => boolean -) => T - = findByCodeLazy("useStateFromStores"); - function Svg(path: string, label: string) { return () => ( <svg diff --git a/src/plugins/spotifyControls/SpotifyStore.ts b/src/plugins/spotifyControls/SpotifyStore.ts index 641ba1ac2..ceac57707 100644 --- a/src/plugins/spotifyControls/SpotifyStore.ts +++ b/src/plugins/spotifyControls/SpotifyStore.ts @@ -76,10 +76,6 @@ export const SpotifyStore = proxyLazy(() => { const API_BASE = "https://api.spotify.com/v1/me/player"; class SpotifyStore extends Store { - constructor(dispatcher: any, handlers: any) { - super(dispatcher, handlers); - } - public mPosition = 0; private start = 0; diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx index 7ab1e3701..5d82998df 100644 --- a/src/plugins/spotifyControls/index.tsx +++ b/src/plugins/spotifyControls/index.tsx @@ -16,16 +16,38 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { Settings } from "@api/settings"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { Player } from "./PlayerComponent"; +function toggleHoverControls(value: boolean) { + document.getElementById("vc-spotify-hover-controls")?.remove(); + if (value) { + const style = document.createElement("style"); + style.id = "vc-spotify-hover-controls"; + style.textContent = ` +.vc-spotify-button-row { height: 0; opacity: 0; will-change: height, opacity; transition: height .2s, opacity .05s; } +#vc-spotify-player:hover .vc-spotify-button-row { opacity: 1; height: 32px; } +`; + document.head.appendChild(style); + } +} + export default definePlugin({ name: "SpotifyControls", description: "Spotify Controls", authors: [Devs.Ven, Devs.afn, Devs.KraXen72], dependencies: ["MenuItemDeobfuscatorAPI"], + options: { + hoverControls: { + description: "Show controls on hover", + type: OptionType.BOOLEAN, + default: false, + onChange: v => toggleHoverControls(v) + }, + }, patches: [ { find: "showTaglessAccountPanel:", @@ -33,7 +55,7 @@ export default definePlugin({ // return React.createElement(AccountPanel, { ..., showTaglessAccountPanel: blah }) match: /return ?(.{0,30}\(.{1,3},\{[^}]+?,showTaglessAccountPanel:.+?\}\))/, // return [Player, Panel] - replace: "return [Vencord.Plugins.plugins.SpotifyControls.renderPlayer(),$1]" + replace: "return [$self.renderPlayer(),$1]" } }, // Adds POST and a Marker to the SpotifyAPI (so we can easily find it) @@ -53,6 +75,6 @@ export default definePlugin({ } } ], - + start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls), renderPlayer: () => <Player /> }); diff --git a/src/plugins/spotifyControls/spotifyStyles.css b/src/plugins/spotifyControls/spotifyStyles.css index 9c7b1c042..63a06e3b0 100644 --- a/src/plugins/spotifyControls/spotifyStyles.css +++ b/src/plugins/spotifyControls/spotifyStyles.css @@ -1,20 +1,22 @@ #vc-spotify-player { padding: 0.375rem 0.5rem; border-bottom: 1px solid var(--background-modifier-accent); + --vc-spotify-green: #1db954; /* so cusotm themes can easily change it */ } + .vc-spotify-button { background: none; color: var(--interactive-normal); padding: 0; width: 32px; height: 32px; - border-radius: 100%; display: flex; justify-content: center; align-items: center; } + .vc-spotify-button:hover { color: var(--interactive-hover); background-color: var(--background-modifier-selected); @@ -24,15 +26,18 @@ height: 24px; width: 24px; } + [class*="vc-spotify-shuffle"] > svg, [class*="vc-spotify-repeat"] > svg { width: 22px; height: 22px; } + .vc-spotify-button svg path { width: 100%; height: 100%; } + /* .vc-spotify-button:hover { filter: brightness(1.3); } */ @@ -51,7 +56,9 @@ white-space: nowrap; padding-right: 0.2em; max-width: 100%; + margin: unset; } + .vc-spotify-repeat-1 { font-size: 70%; position: absolute; @@ -92,15 +99,12 @@ overflow: hidden; } -.vc-spotify-tooltip-text { - margin: unset; -} - #vc-spotify-song-title { color: var(--header-primary); font-size: 14px; font-weight: 600; } + .vc-spotify-ellipoverflow { white-space: nowrap; overflow: hidden; @@ -137,7 +141,6 @@ #vc-spotify-progress-bar { position: relative; - color: var(--text-normal); width: 100%; margin: 0.5em 0; @@ -153,6 +156,7 @@ #vc-spotify-progress-bar > [class^="slider"] [class^="bar-"] { height: 4px !important; } + #vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] { /* these importants are neccessary, it applies a width and height through inline styles */ height: 10px !important; @@ -168,7 +172,6 @@ .vc-spotify-progress-time { font-size: 12px; - top: 10px; position: absolute; } @@ -176,6 +179,7 @@ .vc-spotify-time-left { left: 0; } + .vc-spotify-time-right { right: 0; } diff --git a/src/plugins/startupTimings/index.tsx b/src/plugins/startupTimings/index.tsx index 2ab00a646..5d66f5ffc 100644 --- a/src/plugins/startupTimings/index.tsx +++ b/src/plugins/startupTimings/index.tsx @@ -29,7 +29,7 @@ export default definePlugin({ find: "PAYMENT_FLOW_MODAL_TEST_PAGE,", replacement: { match: /{section:.{1,2}\..{1,3}\.PAYMENT_FLOW_MODAL_TEST_PAGE/, - replace: '{section:"StartupTimings",label:"Startup Timings",element:Vencord.Plugins.plugins.StartupTimings.StartupTimingPage},$&' + replace: '{section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage},$&' } }], StartupTimingPage: LazyComponent(() => require("./StartupTimingPage").default) diff --git a/src/plugins/TimeBarAllActivities.ts b/src/plugins/timeBarAllActivities.ts similarity index 100% rename from src/plugins/TimeBarAllActivities.ts rename to src/plugins/timeBarAllActivities.ts diff --git a/src/plugins/typingIndicator.tsx b/src/plugins/typingIndicator.tsx new file mode 100644 index 000000000..27c143b02 --- /dev/null +++ b/src/plugins/typingIndicator.tsx @@ -0,0 +1,135 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { definePluginSettings, Settings } from "@api/settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { LazyComponent } from "@utils/misc"; +import definePlugin, { OptionType } from "@utils/types"; +import { find, findLazy, findStoreLazy } from "@webpack"; +import { ChannelStore, GuildMemberStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; + +import { buildSeveralUsers } from "./typingTweaks"; + +const ThreeDots = LazyComponent(() => find(m => m.type?.render?.toString()?.includes("().dots"))); + +const TypingStore = findStoreLazy("TypingStore"); +const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); + +const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING); + +function getDisplayName(guildId: string, userId: string) { + return GuildMemberStore.getNick(guildId, userId) ?? UserStore.getUser(userId).username; +} + +function TypingIndicator({ channelId }: { channelId: string; }) { + const typingUsers: Record<string, number> = useStateFromStores( + [TypingStore], + () => ({ ...TypingStore.getTypingUsers(channelId) as Record<string, number> }), + null, + (old, current) => { + const oldKeys = Object.keys(old); + const currentKeys = Object.keys(current); + + return oldKeys.length === currentKeys.length && JSON.stringify(oldKeys) === JSON.stringify(currentKeys); + } + ); + + const guildId = ChannelStore.getChannel(channelId).guild_id; + + if (!settings.store.includeMutedChannels) { + const isChannelMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId); + if (isChannelMuted) return null; + } + + delete typingUsers[UserStore.getCurrentUser().id]; + + const typingUsersArray = Object.keys(typingUsers); + let tooltipText: string; + + switch (typingUsersArray.length) { + case 0: break; + case 1: { + tooltipText = Formatters.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) }); + break; + } + case 2: { + tooltipText = Formatters.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) }); + break; + } + case 3: { + tooltipText = Formatters.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) }); + break; + } + default: { + tooltipText = Settings.plugins.TypingTweaks.enabled + ? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: typingUsersArray.length - 2 }) + : Formatters.Messages.SEVERAL_USERS_TYPING; + break; + } + } + + if (typingUsersArray.length > 0) { + return ( + <Tooltip text={tooltipText!}> + {({ onMouseLeave, onMouseEnter }) => ( + <div + style={{ marginLeft: 6, zIndex: 0, cursor: "pointer" }} + onMouseLeave={onMouseLeave} + onMouseEnter={onMouseEnter} + > + <ThreeDots dotRadius={3} themed={true} /> + </div> + )} + </Tooltip> + ); + } + + return null; +} + +const settings = definePluginSettings({ + includeMutedChannels: { + type: OptionType.BOOLEAN, + description: "Whether to show the typing indicator for muted channels.", + default: false + } +}); + +export default definePlugin({ + name: "TypingIndicator", + description: "Adds an indicator if someone is typing on a channel.", + authors: [Devs.Nuckyz], + settings, + + patches: [ + { + find: ".UNREAD_HIGHLIGHT", + replacement: { + match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/, + replace: ",$self.TypingIndicator($<channel>.id)" + } + } + ], + + TypingIndicator: (channelId: string) => ( + <ErrorBoundary noop> + <TypingIndicator channelId={channelId} /> + </ErrorBoundary> + ), +}); diff --git a/src/plugins/typingTweaks.tsx b/src/plugins/typingTweaks.tsx new file mode 100644 index 000000000..db8c438b5 --- /dev/null +++ b/src/plugins/typingTweaks.tsx @@ -0,0 +1,114 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { definePluginSettings } from "@api/settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy } from "@webpack"; +import { GuildMemberStore, React, RelationshipStore } from "@webpack/common"; +import { User } from "discord-types/general"; + +const Avatar = findByCodeLazy(".Positions.TOP,spacing:"); + +const settings = definePluginSettings({ + showAvatars: { + type: OptionType.BOOLEAN, + default: true, + description: "Show avatars in the typing indicator" + }, + showRoleColors: { + type: OptionType.BOOLEAN, + default: true, + description: "Show role colors in the typing indicator" + }, + alternativeFormatting: { + type: OptionType.BOOLEAN, + default: true, + description: "Show a more useful message when several users are typing" + } +}); + +export function buildSeveralUsers({ a, b, c }: { a: string, b: string, c: number; }) { + return [ + <strong key="0">{a}</strong>, + ", ", + <strong key="2">{b}</strong>, + `, and ${c} others are typing...` + ]; +} + +export default definePlugin({ + name: "TypingTweaks", + description: "Show avatars and role colours in the typing indicator", + authors: [Devs.zt], + patches: [ + // Style the indicator and add function call to modify the children before rendering + { + find: "getCooldownTextStyle", + replacement: { + match: /=(\i)\[2];(.+)"aria-atomic":!0,children:(\i)}\)/, + replace: "=$1[2];$2\"aria-atomic\":!0,style:{display:\"grid\",gridAutoFlow:\"column\",gridGap:\"0.25em\"},children:$self.mutateChildren(this.props,$1,$3)})" + } + }, + // Changes the indicator to keep the user object when creating the list of typing users + { + find: "getCooldownTextStyle", + replacement: { + match: /return \i\.Z\.getName\(.,.\.props\.channel\.id,(.)\)/, + replace: "return $1" + } + }, + // Adds the alternative formatting for several users typing + { + find: "getCooldownTextStyle", + replacement: { + match: /((\i)\.length\?.\..\.Messages\.THREE_USERS_TYPING.format\(\{a:(\i),b:(\i),c:.}\)):.+?SEVERAL_USERS_TYPING/, + replace: "$1:$self.buildSeveralUsers({a:$3,b:$4,c:$2.length-2})" + }, + predicate: () => settings.store.alternativeFormatting + } + ], + settings, + + buildSeveralUsers, + + mutateChildren(props: any, users: User[], children: any) { + if (!Array.isArray(children)) return children; + + let element = 0; + + return children.map(c => c.type === "strong" ? <this.TypingUser {...props} user={users[element++]} /> : c); + }, + + TypingUser: ErrorBoundary.wrap(({ user, guildId }: { user: User, guildId: string; }) => { + return <strong style={{ + display: "grid", + gridAutoFlow: "column", + gap: "4px", + color: settings.store.showRoleColors ? GuildMemberStore.getMember(guildId, user.id)?.colorString : undefined + }}> + {settings.store.showAvatars && <div style={{ marginTop: "4px" }}> + <Avatar + size={Avatar.Sizes.SIZE_16} + src={user.getAvatarURL(guildId, 128)} /> + </div>} + {GuildMemberStore.getNick(guildId!, user.id) || !guildId && RelationshipStore.getNickname(user.id) || user.username} + </strong>; + }, { noop: true }) +}); diff --git a/src/plugins/vcDoubleClick.ts b/src/plugins/vcDoubleClick.ts index de573f1c4..64c676cfc 100644 --- a/src/plugins/vcDoubleClick.ts +++ b/src/plugins/vcDoubleClick.ts @@ -39,30 +39,25 @@ export default definePlugin({ // e.detail since instead of the event they pass the channel. // do this timer workaround instead replacement: [ - // voice channels + // voice/stage channels { - match: /onClick:(.*)function\(\)\{(e\.handleClick.+?)}/g, - replace: "onClick:$1function(){Vencord.Plugins.plugins.VoiceChatDoubleClick.schedule(()=>{$2}, e)}", + match: /onClick:function\(\)\{(e\.handleClick.+?)}/g, + replace: "onClick:function(){$self.schedule(()=>{$1},e)}", }, - // stage channels - { - match: /onClick:(.{0,15})this\.handleClick,/g, - replace: "onClick:$1(...args)=>Vencord.Plugins.plugins.VoiceChatDoubleClick.schedule(()=>{this.handleClick(...args);}, args[0]),", - } ], }, { - find: 'className:"channelMention",iconType:(', + // channel mentions + find: ".EMOJI_IN_MESSAGE_HOVER", replacement: { - match: /onClick:(.{1,3}),/, - replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)(),", + match: /onClick:(\i)(?=,.{0,30}className:"channelMention")/, + replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)()", } } ], schedule(cb: () => void, e: any) { - // support from stage and voice channels patch - const id = e?.id ?? e.props.channel.id as string; + const id = e.props.channel.id as string; if (SelectedChannelStore.getVoiceChannelId() === id) { cb(); return; diff --git a/src/plugins/vcNarrator.tsx b/src/plugins/vcNarrator.tsx new file mode 100644 index 000000000..b2904ac83 --- /dev/null +++ b/src/plugins/vcNarrator.tsx @@ -0,0 +1,328 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { Settings } from "@api/settings"; +import { ErrorCard } from "@components/ErrorCard"; +import { Devs } from "@utils/constants"; +import Logger from "@utils/Logger"; +import { wordsToTitle } from "@utils/text"; +import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Button, ChannelStore, FluxDispatcher, Forms, Margins, SelectedChannelStore, useMemo, UserStore } from "@webpack/common"; + +interface VoiceState { + userId: string; + channelId?: string; + oldChannelId?: string; + deaf: boolean; + mute: boolean; + selfDeaf: boolean; + selfMute: boolean; +} + +const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId"); + +// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying +// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would +// not say the second mute, which would lead you to believe they're unmuted + +function speak(text: string, settings: any = Settings.plugins.VcNarrator) { + if (!text) return; + + const speech = new SpeechSynthesisUtterance(text); + let voice = speechSynthesis.getVoices().find(v => v.voiceURI === settings.voice); + if (!voice) { + new Logger("VcNarrator").error(`Voice "${settings.voice}" not found. Resetting to default.`); + voice = speechSynthesis.getVoices().find(v => v.default); + settings.voice = voice?.voiceURI; + if (!voice) return; // This should never happen + } + speech.voice = voice!; + speech.volume = settings.volume; + speech.rate = settings.rate; + speechSynthesis.speak(speech); +} + +function clean(str: string, fallback: string) { + return str.normalize("NFKC") + .replace(/[^\w ]/g, "") + .trim() + || fallback; +} + +function formatText(str: string, user: string, channel: string) { + return str + .replaceAll("{{USER}}", clean(user, user ? "Someone" : "")) + .replaceAll("{{CHANNEL}}", clean(channel, "channel")); +} + +/* +let StatusMap = {} as Record<string, { + mute: boolean; + deaf: boolean; +}>; +*/ + +// For every user, channelId and oldChannelId will differ when moving channel. +// Only for the local user, channelId and oldChannelId will be the same when moving channel, +// for some ungodly reason +let myLastChannelId: string | undefined; + +function getTypeAndChannelId({ channelId, oldChannelId }: VoiceState, isMe: boolean) { + if (isMe && channelId !== myLastChannelId) { + oldChannelId = myLastChannelId; + myLastChannelId = channelId; + } + + if (channelId !== oldChannelId) { + if (channelId) return [oldChannelId ? "move" : "join", channelId]; + if (oldChannelId) return ["leave", oldChannelId]; + } + /* + if (channelId) { + if (deaf || selfDeaf) return ["deafen", channelId]; + if (mute || selfMute) return ["mute", channelId]; + const oldStatus = StatusMap[userId]; + if (oldStatus.deaf) return ["undeafen", channelId]; + if (oldStatus.mute) return ["unmute", channelId]; + } + */ + return ["", ""]; +} + +/* +function updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId, channelId }: VoiceState, isMe: boolean) { + if (isMe && (type === "join" || type === "move")) { + StatusMap = {}; + const states = VoiceStateStore.getVoiceStatesForChannel(channelId!) as Record<string, VoiceState>; + for (const userId in states) { + const s = states[userId]; + StatusMap[userId] = { + mute: s.mute || s.selfMute, + deaf: s.deaf || s.selfDeaf + }; + } + return; + } + + if (type === "leave" || (type === "move" && channelId !== SelectedChannelStore.getVoiceChannelId())) { + if (isMe) + StatusMap = {}; + else + delete StatusMap[userId]; + + return; + } + + StatusMap[userId] = { + deaf: deaf || selfDeaf, + mute: mute || selfMute + }; +} +*/ + +function handleVoiceStates({ voiceStates }: { voiceStates: VoiceState[]; }) { + const myChanId = SelectedChannelStore.getVoiceChannelId(); + const myId = UserStore.getCurrentUser().id; + + for (const state of voiceStates) { + const { userId, channelId, oldChannelId } = state; + const isMe = userId === myId; + if (!isMe) { + if (!myChanId) continue; + if (channelId !== myChanId && oldChannelId !== myChanId) continue; + } + + const [type, id] = getTypeAndChannelId(state, isMe); + if (!type) continue; + + const template = Settings.plugins.VcNarrator[type + "Message"]; + const user = isMe ? "" : UserStore.getUser(userId).username; + const channel = ChannelStore.getChannel(id).name; + + speak(formatText(template, user, channel)); + + // updateStatuses(type, state, isMe); + } +} + +function handleToggleSelfMute() { + const chanId = SelectedChannelStore.getVoiceChannelId()!; + const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState; + if (!s) return; + + const event = s.mute || s.selfMute ? "unmute" : "mute"; + speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name)); +} + +function handleToggleSelfDeafen() { + const chanId = SelectedChannelStore.getVoiceChannelId()!; + const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState; + if (!s) return; + + const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen"; + speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name)); +} + +function playSample(tempSettings: any, type: string) { + const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings); + + speak(formatText(settings[type + "Message"], UserStore.getCurrentUser().username, "general"), settings); +} + +export default definePlugin({ + name: "VcNarrator", + description: "Announces when users join, leave, or move voice channels via narrator", + authors: [Devs.Ven], + + start() { + if (speechSynthesis.getVoices().length === 0) { + new Logger("VcNarrator").warn("No Narrator voices found. Thus, this plugin will not work. Check my Settings for more info"); + return; + } + FluxDispatcher.subscribe("VOICE_STATE_UPDATES", handleVoiceStates); + FluxDispatcher.subscribe("AUDIO_TOGGLE_SELF_MUTE", handleToggleSelfMute); + FluxDispatcher.subscribe("AUDIO_TOGGLE_SELF_DEAF", handleToggleSelfDeafen); + }, + + stop() { + FluxDispatcher.unsubscribe("VOICE_STATE_UPDATES", handleVoiceStates); + FluxDispatcher.subscribe("AUDIO_TOGGLE_SELF_MUTE", handleToggleSelfMute); + FluxDispatcher.subscribe("AUDIO_TOGGLE_SELF_DEAF", handleToggleSelfDeafen); + }, + + optionsCache: null as Record<string, PluginOptionsItem> | null, + + get options() { + return this.optionsCache ??= { + voice: { + type: OptionType.SELECT, + description: "Narrator Voice", + options: speechSynthesis.getVoices().map(v => ({ + label: v.name, + value: v.voiceURI, + default: v.default + })) + }, + volume: { + type: OptionType.SLIDER, + description: "Narrator Volume", + default: 1, + markers: [0, 0.25, 0.5, 0.75, 1], + stickToMarkers: false + }, + rate: { + type: OptionType.SLIDER, + description: "Narrator Speed", + default: 1, + markers: [0.1, 0.5, 1, 2, 5, 10], + stickToMarkers: false + }, + joinMessage: { + type: OptionType.STRING, + description: "Join Message", + default: "{{USER}} joined" + }, + leaveMessage: { + type: OptionType.STRING, + description: "Leave Message", + default: "{{USER}} left" + }, + moveMessage: { + type: OptionType.STRING, + description: "Move Message", + default: "{{USER}} moved to {{CHANNEL}}" + }, + muteMessage: { + type: OptionType.STRING, + description: "Mute Message (only self for now)", + default: "{{USER}} Muted" + }, + unmuteMessage: { + type: OptionType.STRING, + description: "Unmute Message (only self for now)", + default: "{{USER}} unmuted" + }, + deafenMessage: { + type: OptionType.STRING, + description: "Deafen Message (only self for now)", + default: "{{USER}} deafened" + }, + undeafenMessage: { + type: OptionType.STRING, + description: "Undeafen Message (only self for now)", + default: "{{USER}} undeafened" + } + }; + }, + + settingsAboutComponent({ tempSettings: s }) { + const [hasVoices, hasEnglishVoices] = useMemo(() => { + const voices = speechSynthesis.getVoices(); + return [voices.length !== 0, voices.some(v => v.lang.startsWith("en"))]; + }, []); + + const types = useMemo( + () => Object.keys(Vencord.Plugins.plugins.VcNarrator.options!).filter(k => k.endsWith("Message")).map(k => k.slice(0, -7)), + [], + ); + + let errorComponent: React.ReactElement | null = null; + if (!hasVoices) { + let error = "No narrator voices found. "; + error += navigator.platform?.toLowerCase().includes("linux") + ? "Install speech-dispatcher or espeak and run Discord with the --enable-speech-dispatcher flag" + : "Try installing some in the Narrator settings of your Operating System"; + errorComponent = <ErrorCard>{error}</ErrorCard>; + } else if (!hasEnglishVoices) { + errorComponent = <ErrorCard>You don't have any English voices installed, so the narrator might sound weird</ErrorCard>; + } + + return ( + <Forms.FormSection> + <Forms.FormText> + You can customise the spoken messages below. You can disable specific messages by setting them to nothing + </Forms.FormText> + <Forms.FormText> + The special placeholders <code>{"{{USER}}"}</code> and <code>{"{{CHANNEL}}"}</code>{" "} + will be replaced with the user's name (nothing if it's yourself) and the channel's name respectively + </Forms.FormText> + {hasEnglishVoices && ( + <> + <Forms.FormTitle className={Margins.marginTop20} tag="h3">Play Example Sounds</Forms.FormTitle> + <div + style={{ + display: "grid", + gridTemplateColumns: "repeat(4, 1fr)", + gap: "1rem", + }} + className={"vc-narrator-buttons"} + > + {types.map(t => ( + <Button key={t} onClick={() => playSample(s, t)}> + {wordsToTitle([t])} + </Button> + ))} + </div> + </> + )} + {errorComponent} + </Forms.FormSection> + ); + } +}); diff --git a/src/plugins/viewIcons.tsx b/src/plugins/viewIcons.tsx index 1b96527f2..26f29029d 100644 --- a/src/plugins/viewIcons.tsx +++ b/src/plugins/viewIcons.tsx @@ -20,11 +20,11 @@ import { Devs } from "@utils/constants"; import { LazyComponent } from "@utils/misc"; import { ModalRoot, ModalSize, openModal } from "@utils/modal"; import { PluginDef } from "@utils/types"; -import { find, findByPropsLazy } from "@webpack"; +import { find, findByCode, findByPropsLazy } from "@webpack"; import { Menu } from "@webpack/common"; import type { Guild } from "discord-types/general"; -const ImageModal = LazyComponent(() => find(m => m.prototype?.render?.toString().includes("OPEN_ORIGINAL_IMAGE"))); +const ImageModal = LazyComponent(() => findByCode(".MEDIA_MODAL_CLOSE,")); const MaskedLink = LazyComponent(() => find(m => m.type?.toString().includes("MASKED_LINK)"))); const GuildBannerStore = findByPropsLazy("getGuildBannerURL"); @@ -48,7 +48,7 @@ export default new class ViewIcons implements PluginDef { shouldAnimate={true} original={url} src={url} - renderLinkComponent={() => <MaskedLink />} + renderLinkComponent={MaskedLink} /> </ModalRoot> )); @@ -63,7 +63,7 @@ export default new class ViewIcons implements PluginDef { replace: (_, src) => `{src:${src},onClick:()=>${OPEN_URL}${src}),avatarDecoration` } }, { - find: "().popoutNoBannerPremium", + find: ".popoutNoBannerPremium", replacement: { match: /style:.{0,10}\{\},(.{1,2})\)/, replace: (m, style) => @@ -79,7 +79,7 @@ export default new class ViewIcons implements PluginDef { }, { match: /(id:"leave-guild".{0,200}),(\(0,.{1,3}\.jsxs?\).{0,200}function)/, - replace: "$1,Vencord.Plugins.plugins.ViewIcons.buildGuildContextMenuEntries(_guild),$2" + replace: "$1,$self.buildGuildContextMenuEntries(_guild),$2" } ] } diff --git a/src/plugins/viewRaw.tsx b/src/plugins/viewRaw.tsx index fc7a42a00..510520312 100644 --- a/src/plugins/viewRaw.tsx +++ b/src/plugins/viewRaw.tsx @@ -98,7 +98,7 @@ function openViewRawModal(msg: Message) { <> <Forms.FormTitle tag="h5">Content</Forms.FormTitle> <CodeBlock content={msg.content} lang="" /> - <Forms.FormDivider classes={Margins.marginBottom20} /> + <Forms.FormDivider className={Margins.marginBottom20} /> </> )} diff --git a/src/plugins/volumeBooster.ts b/src/plugins/volumeBooster.ts index dab6b930f..6553a5c78 100644 --- a/src/plugins/volumeBooster.ts +++ b/src/plugins/volumeBooster.ts @@ -26,25 +26,51 @@ export default definePlugin({ description: "Allows you to set the user and stream volume above the default maximum.", patches: [ - { - find: ".Messages.USER_VOLUME", + // Change the max volume for sliders to allow for values above 200 + ...[ + ".Messages.USER_VOLUME", + "currentVolume:" + ].map(find => ({ + find, replacement: { - match: /maxValue:(.{1,2}\..{1,2})\?(\d+?):(\d+?),/, - replace: (_, defaultMaxVolumePredicate, higherMaxVolume, minorMaxVolume) => "" - + `maxValue:${defaultMaxVolumePredicate}` - + `?${higherMaxVolume}*Vencord.Settings.plugins.VolumeBooster.multiplier` - + `:${minorMaxVolume}*Vencord.Settings.plugins.VolumeBooster.multiplier,` + match: /maxValue:(?<defaultMaxVolumePredicate>\i\.\i)\?(?<higherMaxVolume>\d+?):(?<minorMaxVolume>\d+?),/, + replace: "" + + "maxValue:$<defaultMaxVolumePredicate>" + + "?$<higherMaxVolume>*Vencord.Settings.plugins.VolumeBooster.multiplier" + + ":$<minorMaxVolume>*Vencord.Settings.plugins.VolumeBooster.multiplier," } + })), + // Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord + { + find: "AudioContextSettingsMigrated", + replacement: [ + { + match: /(?<restOfFunction>updateAsync\("audioContextSettings".{1,50})(?<volumeChangeExpression>return (?<volumeOptions>\i)\.volume=(?<newVolume>\i))/, + replace: "$<restOfFunction>if($<newVolume>>200)return $<volumeOptions>.volume=200;$<volumeChangeExpression>" + }, + { + match: /(?<restOfFunction>Object\.entries\(\i\.localMutes\).+?)volume:(?<volumeExpression>.+?),/, + replace: "$<restOfFunction>volume:$<volumeExpression>>200?200:$<volumeExpression>," + }, + { + match: /(?<restOfFunction>Object\.entries\(\i\.localVolumes\).+?)volume:(?<volumeExpression>.+?)}\)/, + replace: "$<restOfFunction>volume:$<volumeExpression>>200?200:$<volumeExpression>})" + } + ] }, + // Prevent the MediaEngineStore from overwriting our LocalVolumes above 200 with the ones the Discord Audio Context Settings sync sends { - find: "currentVolume:", - replacement: { - match: /maxValue:(.{1,2}\..{1,2})\?(\d+?):(\d+?),/, - replace: (_, defaultMaxVolumePredicate, higherMaxVolume, minorMaxVolume) => "" - + `maxValue:${defaultMaxVolumePredicate}` - + `?${higherMaxVolume}*Vencord.Settings.plugins.VolumeBooster.multiplier` - + `:${minorMaxVolume}*Vencord.Settings.plugins.VolumeBooster.multiplier,` - } + find: '.displayName="MediaEngineStore"', + replacement: [ + { + match: /(?<restOfFunction>\.settings\.audioContextSettings.+?)(?<localVolume>\i\[\i\])=(?<syncVolume>\i\.volume)(?<secondRestOfFunction>.+?)setLocalVolume\((?<id>.+?),.+?\)/, + replace: "" + + "$<restOfFunction>" + + "($<localVolume>>200?undefined:$<localVolume>=$<syncVolume>)" + + "$<secondRestOfFunction>" + + "setLocalVolume($<id>,$<localVolume>??$<syncVolume>)" + } + ] } ], diff --git a/src/plugins/whoReacted.tsx b/src/plugins/whoReacted.tsx index 685d95421..8ab1c5f7d 100644 --- a/src/plugins/whoReacted.tsx +++ b/src/plugins/whoReacted.tsx @@ -32,12 +32,13 @@ const ReactionStore = findByPropsLazy("getReactions"); const queue = new Queue(); -function fetchReactions(msg: Message, emoji: ReactionEmoji) { +function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) { const key = emoji.name + (emoji.id ? `:${emoji.id}` : ""); return RestAPI.get({ url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`, query: { - limit: 100 + limit: 100, + type }, oldFormErrors: true }) @@ -46,18 +47,19 @@ function fetchReactions(msg: Message, emoji: ReactionEmoji) { channelId: msg.channel_id, messageId: msg.id, users: res.body, - emoji + emoji, + reactionType: type })) .catch(console.error) .finally(() => sleep(250)); } -function getReactionsWithQueue(msg: Message, e: ReactionEmoji) { - const key = `${msg.id}:${e.name}:${e.id ?? ""}`; +function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) { + const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`; const cache = ReactionStore.__getLocalVars().reactions[key] ??= { fetched: false, users: {} }; if (!cache.fetched) { queue.unshift(() => - fetchReactions(msg, e) + fetchReactions(msg, e, type) ); cache.fetched = true; } @@ -92,7 +94,7 @@ export default definePlugin({ find: ",reactionRef:", replacement: { match: /((.)=(.{1,3})\.hideCount)(,.+?reactionCount.+?\}\))/, - replace: "$1,whoReactedProps=$3$4,$2?null:Vencord.Plugins.plugins.WhoReacted.renderUsers(whoReactedProps)" + replace: "$1,whoReactedProps=$3$4,$2?null:$self.renderUsers(whoReactedProps)" } }], @@ -104,7 +106,7 @@ export default definePlugin({ ); }, - _renderUsers({ message, emoji }: RootObject) { + _renderUsers({ message, emoji, type }: RootObject) { const forceUpdate = useForceUpdater(); React.useEffect(() => { const cb = (e: any) => { @@ -116,9 +118,16 @@ export default definePlugin({ return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb); }, [message.id]); - const reactions = getReactionsWithQueue(message, emoji); + const reactions = getReactionsWithQueue(message, emoji, type); const users = Object.values(reactions).filter(Boolean) as User[]; + for (const user of users) { + FluxDispatcher.dispatch({ + type: "USER_UPDATE", + user + }); + } + return ( <div style={{ marginLeft: "0.5em", transform: "scale(0.9)" }} diff --git a/src/preload.ts b/src/preload.ts index 746008142..820b65547 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -18,27 +18,12 @@ import { debounce } from "@utils/debounce"; import IpcEvents from "@utils/IpcEvents"; -import electron, { contextBridge, ipcRenderer, webFrame } from "electron"; -import { readFileSync } from "fs"; +import { contextBridge, webFrame } from "electron"; +import { readFileSync, watch } from "fs"; import { join } from "path"; import VencordNative from "./VencordNative"; -if (electron.desktopCapturer === void 0) { - // Fix for desktopCapturer being main only in Electron 17+ - // Discord accesses this in discord_desktop_core (DiscordNative.desktopCapture.getDesktopCaptureSources) - // and errors with cannot "read property getSources() of undefined" - // see discord_desktop_core/app/discord_native/renderer/desktopCapture.js - const electronPath = require.resolve("electron"); - delete require.cache[electronPath]!.exports; - require.cache[electronPath]!.exports = { - ...electron, - desktopCapturer: { - getSources: opts => ipcRenderer.invoke(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, opts) - } - }; -} - contextBridge.exposeInMainWorld("VencordNative", VencordNative); if (location.protocol !== "data:") { @@ -60,17 +45,14 @@ if (location.protocol !== "data:") { } } - try { - const css = readFileSync(rendererCss, "utf-8"); - insertCss(css); - } catch (err) { - if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") - throw err; - - // hack: the pre update updater does not download this file, so manually download it - // TODO: remove this in a future version - ipcRenderer.invoke(IpcEvents.DOWNLOAD_VENCORD_CSS) - .then(insertCss); + const css = readFileSync(rendererCss, "utf-8"); + insertCss(css); + if (IS_DEV) { + // persistent means keep process running if watcher is the only thing still running + // which we obviously don't want + watch(rendererCss, { persistent: false }, () => { + document.getElementById("vencord-css-core")!.textContent = readFileSync(rendererCss, "utf-8"); + }); } require(process.env.DISCORD_PRELOAD!); } else { diff --git a/src/utils/IpcEvents.ts b/src/utils/IpcEvents.ts index 345146b22..dbf3540c3 100644 --- a/src/utils/IpcEvents.ts +++ b/src/utils/IpcEvents.ts @@ -43,7 +43,5 @@ export default strEnum({ GET_HASHES: "VencordGetHashes", UPDATE: "VencordUpdate", BUILD: "VencordBuild", - GET_DESKTOP_CAPTURE_SOURCES: "VencordGetDesktopCaptureSources", OPEN_MONACO_EDITOR: "VencordOpenMonacoEditor", - DOWNLOAD_VENCORD_CSS: "VencordDownloadVencordCss" } as const); diff --git a/src/utils/Queue.ts b/src/utils/Queue.ts index 86eb79196..2680f5636 100644 --- a/src/utils/Queue.ts +++ b/src/utils/Queue.ts @@ -27,7 +27,7 @@ export class Queue { * @param maxSize The maximum amount of functions that can be queued at once. * If the queue is full, the oldest function will be removed. */ - constructor(public maxSize = Infinity) { } + constructor(public readonly maxSize = Infinity) { } private queue = [] as Array<() => Promisable<unknown>>; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d15615b60..4f24d03b4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -180,5 +180,25 @@ export const Devs = /* #__PURE__*/ Object.freeze({ pointy: { name: "pointy", id: 99914384989519872n + }, + SammCheese: { + name: "Samm-Cheese", + id: 372148345894076416n + }, + zt: { + name: "zt", + id: 289556910426816513n + }, + captain: { + name: "Captain", + id: 347366054806159360n + }, + whqwert: { + name: "whqwert", + id: 586239091520176128n + }, + lewisakura: { + name: "lewisakura", + id: 96269247411400704n } }); diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts index ed26644ea..a09a87b2f 100644 --- a/src/utils/dependencies.ts +++ b/src/utils/dependencies.ts @@ -78,3 +78,6 @@ export interface ApngFrameData { const shikiWorkerDist = "https://unpkg.com/@vap/shiki-worker@0.0.8/dist"; export const shikiWorkerSrc = `${shikiWorkerDist}/${IS_DEV ? "index.js" : "index.min.js"}`; export const shikiOnigasmSrc = "https://unpkg.com/@vap/shiki@0.10.3/dist/onig.wasm"; + +// @ts-expect-error SHUT UP +export const getStegCloak = makeLazy(() => import("https://unpkg.com/stegcloak-dist@1.0.0/index.js")); diff --git a/src/utils/index.ts b/src/utils/index.ts index 41e1597a1..cfded6b5d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -22,9 +22,11 @@ export * from "./debounce"; export * as Discord from "./discord"; export { default as IpcEvents } from "./IpcEvents"; export { default as Logger } from "./Logger"; +export * from "./margins"; export * from "./misc"; export * as Modals from "./modal"; export * from "./onceDefined"; export * from "./proxyLazy"; export * from "./Queue"; +export * from "./text"; diff --git a/src/utils/margins.ts b/src/utils/margins.ts new file mode 100644 index 000000000..5d7eed766 --- /dev/null +++ b/src/utils/margins.ts @@ -0,0 +1,35 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +let styleStr = ""; + +export const Margins: Record<`${"top" | "bottom" | "left" | "right"}${8 | 16 | 20}`, string> = {} as any; + +for (const dir of ["top", "bottom", "left", "right"] as const) { + for (const size of [8, 16, 20] as const) { + const cl = `vc-m-${dir}-${size}`; + Margins[`${dir}${size}`] = cl; + styleStr += `.${cl}{margin-${dir}:${size}px;}`; + } +} + +document.addEventListener("DOMContentLoaded", () => + document.head.append(Object.assign(document.createElement("style"), { + textContent: styleStr, + id: "vencord-margins" + })), { once: true }); diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 6710523a7..a41ab6730 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -142,7 +142,7 @@ export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string = * classes("one", "two") => "one two" */ export function classes(...classes: string[]) { - return classes.join(" "); + return classes.filter(c => typeof c === "string").join(" "); } /** @@ -152,34 +152,6 @@ export function sleep(ms: number): Promise<void> { return new Promise(r => setTimeout(r, ms)); } -/** - * Wraps a Function into a try catch block and logs any errors caught - * Due to the nature of this function, not all paths return a result. - * Thus, for consistency, the returned functions will always return void or Promise<void> - * - * @param name Name identifying the wrapped function. This will appear in the logged errors - * @param func Function (async or sync both work) - * @param thisObject Optional thisObject - * @returns Wrapped Function - */ -export function suppressErrors<F extends Function>(name: string, func: F, thisObject?: any): F { - return (func.constructor.name === "AsyncFunction" - ? async function (this: any) { - try { - await func.apply(thisObject ?? this, arguments); - } catch (e) { - console.error(`Caught an Error in ${name || "anonymous"}\n`, e); - } - } - : function (this: any) { - try { - func.apply(thisObject ?? this, arguments); - } catch (e) { - console.error(`Caught an Error in ${name || "anonymous"}\n`, e); - } - }) as any as F; -} - /** * Wrap the text in ``` with an optional language */ @@ -228,3 +200,7 @@ export const checkIntersecting = (el: Element) => { const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0); }; + +export function identity<T>(value: T): T { + return value; +} diff --git a/src/utils/modal.tsx b/src/utils/modal.tsx index 9a90460b3..3174cace0 100644 --- a/src/utils/modal.tsx +++ b/src/utils/modal.tsx @@ -17,6 +17,9 @@ */ import { filters, mapMangledModuleLazy } from "@webpack"; +import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react"; + +import { LazyComponent } from "./misc"; export enum ModalSize { SMALL = "small", @@ -44,30 +47,71 @@ export interface ModalOptions { onCloseCallback?: (() => void); } -interface ModalRootProps { - transitionState: ModalTransitionState; - children: React.ReactNode; - size?: ModalSize; - role?: "alertdialog" | "dialog"; - className?: string; - onAnimationEnd?(): string; -} +type RenderFunction = (props: ModalProps) => ReactNode; -type RenderFunction = (props: ModalProps) => React.ReactNode; +export const Modals = mapMangledModuleLazy(".closeWithCircleBackground", { + ModalRoot: filters.byCode(".root"), + ModalHeader: filters.byCode(".header"), + ModalContent: filters.byCode(".content"), + ModalFooter: filters.byCode(".footerSeparator"), + ModalCloseButton: filters.byCode(".closeWithCircleBackground"), +}) as { + ModalRoot: ComponentType<PropsWithChildren<{ + transitionState: ModalTransitionState; + size?: ModalSize; + role?: "alertdialog" | "dialog"; + className?: string; + fullscreenOnMobile?: boolean; + "aria-label"?: string; + "aria-labelledby"?: string; + onAnimationEnd?(): string; + }>>; + ModalHeader: ComponentType<PropsWithChildren<{ + /** Flex.Justify.START */ + justify?: string; + /** Flex.Direction.HORIZONTAL */ + direction?: string; + /** Flex.Align.CENTER */ + align?: string; + /** Flex.Wrap.NO_WRAP */ + wrap?: string; + separator?: boolean; -export const Modals = mapMangledModuleLazy("().closeWithCircleBackground", { - ModalRoot: filters.byCode("().root"), - ModalHeader: filters.byCode("().header"), - ModalContent: filters.byCode("().content"), - ModalFooter: filters.byCode("().footerSeparator"), - ModalCloseButton: filters.byCode("().closeWithCircleBackground"), -}); + className?: string; + }>>; + /** This also accepts Scroller props but good luck with that */ + ModalContent: ComponentType<PropsWithChildren<{ + className?: string; + scrollerRef?: Ref<HTMLElement>; + [prop: string]: any; + }>>; + ModalFooter: ComponentType<PropsWithChildren<{ + /** Flex.Justify.START */ + justify?: string; + /** Flex.Direction.HORIZONTAL_REVERSE */ + direction?: string; + /** Flex.Align.STRETCH */ + align?: string; + /** Flex.Wrap.NO_WRAP */ + wrap?: string; + separator?: boolean; -export const ModalRoot = (props: ModalRootProps) => <Modals.ModalRoot {...props} />; -export const ModalHeader = (props: any) => <Modals.ModalHeader {...props} />; -export const ModalContent = (props: any) => <Modals.ModalContent {...props} />; -export const ModalFooter = (props: any) => <Modals.ModalFooter {...props} />; -export const ModalCloseButton = (props: any) => <Modals.ModalCloseButton {...props} />; + className?: string; + }>>; + ModalCloseButton: ComponentType<{ + focusProps?: any; + onClick(): void; + withCircleBackground?: boolean; + hideOnFullscreen?: boolean; + className?: string; + }>; +}; + +export const ModalRoot = LazyComponent(() => Modals.ModalRoot); +export const ModalHeader = LazyComponent(() => Modals.ModalHeader); +export const ModalContent = LazyComponent(() => Modals.ModalContent); +export const ModalFooter = LazyComponent(() => Modals.ModalFooter); +export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton); const ModalAPI = mapMangledModuleLazy("onCloseRequest:null!=", { openModal: filters.byCode("onCloseRequest:null!="), diff --git a/src/utils/proxyLazy.ts b/src/utils/proxyLazy.ts index 42b5a91d3..b1fca6e77 100644 --- a/src/utils/proxyLazy.ts +++ b/src/utils/proxyLazy.ts @@ -22,6 +22,9 @@ const unconfigurable = ["arguments", "caller", "prototype"]; const handler: ProxyHandler<any> = {}; +const GET_KEY = Symbol.for("vencord.lazy.get"); +const CACHED_KEY = Symbol.for("vencord.lazy.cached"); + for (const method of [ "apply", "construct", @@ -38,11 +41,11 @@ for (const method of [ "setPrototypeOf" ]) { handler[method] = - (target: any, ...args: any[]) => Reflect[method](target.get(), ...args); + (target: any, ...args: any[]) => Reflect[method](target[GET_KEY](), ...args); } handler.ownKeys = target => { - const v = target.get(); + const v = target[GET_KEY](); const keys = Reflect.ownKeys(v); for (const key of unconfigurable) { if (!keys.includes(key)) keys.push(key); @@ -54,7 +57,10 @@ handler.getOwnPropertyDescriptor = (target, p) => { if (typeof p === "string" && unconfigurable.includes(p)) return Reflect.getOwnPropertyDescriptor(target, p); - return Reflect.getOwnPropertyDescriptor(target.get(), p); + const descriptor = Reflect.getOwnPropertyDescriptor(target[GET_KEY](), p); + + if (descriptor) Object.defineProperty(target, p, descriptor); + return descriptor; }; /** @@ -67,10 +73,10 @@ handler.getOwnPropertyDescriptor = (target, p) => { * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); */ export function proxyLazy<T>(factory: () => T): T { - const proxyDummy: { (): void; cachedValue?: T; get(): T; } = function () { }; - - proxyDummy.cachedValue = void 0; - proxyDummy.get = () => proxyDummy.cachedValue ??= factory(); + const proxyDummy: { (): void; [CACHED_KEY]?: T; [GET_KEY](): T; } = Object.assign(function () { }, { + [CACHED_KEY]: void 0, + [GET_KEY]: () => proxyDummy[CACHED_KEY] ??= factory(), + }); return new Proxy(proxyDummy, handler) as any; } diff --git a/src/utils/quickCss.ts b/src/utils/quickCss.ts index de4eaefbc..4ae102333 100644 --- a/src/utils/quickCss.ts +++ b/src/utils/quickCss.ts @@ -29,10 +29,10 @@ export async function toggle(isEnabled: boolean) { style = document.createElement("style"); style.id = "vencord-custom-css"; document.head.appendChild(style); - VencordNative.ipc.on(IpcEvents.QUICK_CSS_UPDATE, (_, css: string) => style.innerText = css); + VencordNative.ipc.on(IpcEvents.QUICK_CSS_UPDATE, (_, css: string) => style.textContent = css); style.textContent = await VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS); } - } else // @ts-ignore yes typescript, property 'disabled' does exist on type 'HTMLStyleElement' u should try reading the docs some time + } else style.disabled = !isEnabled; } diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 5cd81e7e4..18e185478 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -112,7 +112,6 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> { if (file) { try { - console.log(file); await importSettings(new TextDecoder().decode(file.data)); if (showToast) toastSuccess(); } catch (err) { diff --git a/src/utils/text.ts b/src/utils/text.ts index 17826e80d..115b3e2ac 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -16,6 +16,8 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { moment } from "@webpack/common"; + // Utils for readable text transformations eg: `toTitle(fromKebab())` // Case style to words @@ -34,3 +36,59 @@ export const wordsToPascal = (words: string[]) => words.map(w => w[0].toUpperCase() + w.slice(1)).join(""); export const wordsToTitle = (words: string[]) => words.map(w => w[0].toUpperCase() + w.slice(1)).join(" "); + +const units = ["years", "months", "weeks", "days", "hours", "minutes", "seconds"] as const; +type Units = typeof units[number]; + +function getUnitStr(unit: Units, isOne: boolean, short: boolean) { + if (short === false) return isOne ? unit.slice(0, -1) : unit; + + return unit[0]; +} + +/** + * Forms time into a human readable string link "1 day, 2 hours, 3 minutes and 4 seconds" + * @param time The time on the specified unit + * @param unit The unit the time is on + * @param short Whether to use short units like "d" instead of "days" + */ +export function formatDuration(time: number, unit: Units, short: boolean = false) { + const dur = moment.duration(time, unit); + + let unitsAmounts = units.map(unit => ({ amount: dur[unit](), unit })); + + let amountsToBeRemoved = 0; + + outer: + for (let i = 0; i < unitsAmounts.length; i++) { + if (unitsAmounts[i].amount === 0 || !(i + 1 < unitsAmounts.length)) continue; + for (let v = i + 1; v < unitsAmounts.length; v++) { + if (unitsAmounts[v].amount !== 0) continue outer; + } + + amountsToBeRemoved = unitsAmounts.length - (i + 1); + } + unitsAmounts = amountsToBeRemoved === 0 ? unitsAmounts : unitsAmounts.slice(0, -amountsToBeRemoved); + + const daysAmountIndex = unitsAmounts.findIndex(({ unit }) => unit === "days"); + if (daysAmountIndex !== -1) { + const daysAmount = unitsAmounts[daysAmountIndex]; + + const daysMod = daysAmount.amount % 7; + if (daysMod === 0) unitsAmounts.splice(daysAmountIndex, 1); + else daysAmount.amount = daysMod; + } + + let res: string = ""; + while (unitsAmounts.length) { + const { amount, unit } = unitsAmounts.shift()!; + + if (res.length) res += unitsAmounts.length ? ", " : " and "; + + if (amount > 0 || res.length) { + res += `${amount} ${getUnitStr(unit, amount === 1, short)}`; + } + } + + return res.length ? res : `0 ${getUnitStr(unit, false, short)}`; +} diff --git a/src/utils/types.ts b/src/utils/types.ts index d3083fcab..5ab685761 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -81,8 +81,14 @@ export interface PluginDef { target?: "WEB" | "DESKTOP" | "BOTH"; /** * Optionally provide settings that the user can configure in the Plugins tab of settings. + * @deprecated Use `settings` instead */ + // TODO: Remove when everything is migrated to `settings` options?: Record<string, PluginOptionsItem>; + /** + * Optionally provide settings that the user can configure in the Plugins tab of settings. + */ + settings?: DefinedSettings; /** * Check that this returns true before allowing a save to complete. * If a string is returned, show the error to the user. @@ -107,19 +113,25 @@ export enum OptionType { COMPONENT, } -export type PluginOptionsItem = - | PluginOptionString - | PluginOptionNumber - | PluginOptionBoolean - | PluginOptionSelect - | PluginOptionSlider - | PluginOptionComponent; +export type SettingsDefinition = Record<string, PluginSettingDef>; +export type SettingsChecks<D extends SettingsDefinition> = { + [K in keyof D]?: D[K] extends PluginSettingComponentDef ? IsDisabled<DefinedSettings<D>> : + (IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>); +}; -export interface PluginOptionBase { +export type PluginSettingDef = ( + | PluginSettingStringDef + | PluginSettingNumberDef + | PluginSettingBooleanDef + | PluginSettingSelectDef + | PluginSettingSliderDef + | PluginSettingComponentDef +) & PluginSettingCommon; + +export interface PluginSettingCommon { description: string; placeholder?: string; onChange?(newValue: any): void; - disabled?(): boolean; restartNeeded?: boolean; componentProps?: Record<string, any>; /** @@ -127,49 +139,47 @@ export interface PluginOptionBase { */ target?: "WEB" | "DESKTOP" | "BOTH"; } - -export interface PluginOptionString extends PluginOptionBase { - type: OptionType.STRING; +interface IsDisabled<D = unknown> { + /** + * Checks if this setting should be disabled + */ + disabled?(this: D): boolean; +} +interface IsValid<T, D = unknown> { /** * Prevents the user from saving settings if this is false or a string */ - isValid?(value: string): boolean | string; + isValid?(this: D, value: T): boolean | string; +} + +export interface PluginSettingStringDef { + type: OptionType.STRING; default?: string; } - -export interface PluginOptionNumber extends PluginOptionBase { - type: OptionType.NUMBER | OptionType.BIGINT; - /** - * Prevents the user from saving settings if this is false or a string - */ - isValid?(value: number | BigInt): boolean | string; +export interface PluginSettingNumberDef { + type: OptionType.NUMBER; default?: number; } - -export interface PluginOptionBoolean extends PluginOptionBase { +export interface PluginSettingBigIntDef { + type: OptionType.BIGINT; + default?: BigInt; +} +export interface PluginSettingBooleanDef { type: OptionType.BOOLEAN; - /** - * Prevents the user from saving settings if this is false or a string - */ - isValid?(value: boolean): boolean | string; default?: boolean; } -export interface PluginOptionSelect extends PluginOptionBase { +export interface PluginSettingSelectDef { type: OptionType.SELECT; - /** - * Prevents the user from saving settings if this is false or a string - */ - isValid?(value: PluginOptionSelectOption): boolean | string; - options: PluginOptionSelectOption[]; + options: readonly PluginSettingSelectOption[]; } -export interface PluginOptionSelectOption { +export interface PluginSettingSelectOption { label: string; value: string | number | boolean; default?: boolean; } -export interface PluginOptionSlider extends PluginOptionBase { +export interface PluginSettingSliderDef { type: OptionType.SLIDER; /** * All the possible values in the slider. Needs at least two values. @@ -183,10 +193,6 @@ export interface PluginOptionSlider extends PluginOptionBase { * If false, allow users to select values in-between your markers. */ stickToMarkers?: boolean; - /** - * Prevents the user from saving settings if this is false or a string - */ - isValid?(value: number): boolean | string; } interface IPluginOptionComponentProps { @@ -206,12 +212,67 @@ interface IPluginOptionComponentProps { /** * The options object */ - option: PluginOptionComponent; + option: PluginSettingComponentDef; } -export interface PluginOptionComponent extends PluginOptionBase { +export interface PluginSettingComponentDef { type: OptionType.COMPONENT; component: (props: IPluginOptionComponentProps) => JSX.Element; } +/** Maps a `PluginSettingDef` to its value type */ +type PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStringDef ? string : + O extends PluginSettingNumberDef ? number : + O extends PluginSettingBigIntDef ? BigInt : + O extends PluginSettingBooleanDef ? boolean : + O extends PluginSettingSelectDef ? O["options"][number]["value"] : + O extends PluginSettingSliderDef ? number : + O extends PluginSettingComponentDef ? any : + never; + +type SettingsStore<D extends SettingsDefinition> = { + [K in keyof D]: PluginSettingType<D[K]>; +}; + +/** An instance of defined plugin settings */ +export interface DefinedSettings<D extends SettingsDefinition = SettingsDefinition, C extends SettingsChecks<D> = {}> { + /** Shorthand for `Vencord.Settings.plugins.PluginName`, but with typings */ + store: SettingsStore<D>; + /** + * React hook for getting the settings for this plugin + * @param filter optional filter to avoid rerenders for irrelavent settings + */ + use<F extends Extract<keyof D, string>>(filter?: F[]): Pick<SettingsStore<D>, F>; + /** Definitions of each setting */ + def: D; + /** Setting methods with return values that could rely on other settings */ + checks: C; + /** + * Name of the plugin these settings belong to, + * will be an empty string until plugin is initialized + */ + pluginName: string; +} + +export type PartialExcept<T, R extends keyof T> = Partial<T> & Required<Pick<T, R>>; + export type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; }; + +/* -------------------------------------------- */ +/* Legacy Options Types */ +/* -------------------------------------------- */ + +export type PluginOptionBase = PluginSettingCommon & IsDisabled; +export type PluginOptionsItem = + | PluginOptionString + | PluginOptionNumber + | PluginOptionBoolean + | PluginOptionSelect + | PluginOptionSlider + | PluginOptionComponent; +export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>; +export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>; +export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>; +export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>; +export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>; +export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon; diff --git a/src/utils/updater.ts b/src/utils/updater.ts index 04205a556..9fdec471e 100644 --- a/src/utils/updater.ts +++ b/src/utils/updater.ts @@ -22,7 +22,7 @@ import IpcEvents from "./IpcEvents"; import Logger from "./Logger"; import { IpcRes } from "./types"; -export const UpdateLogger = new Logger("Updater", "white"); +export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white"); export let isOutdated = false; export let isNewer = false; export let updateError: any; diff --git a/src/webpack/common.tsx b/src/webpack/common.tsx deleted file mode 100644 index a732d6b7c..000000000 --- a/src/webpack/common.tsx +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { LazyComponent } from "@utils/misc"; -import { proxyLazy } from "@utils/proxyLazy"; -import { - _resolveReady, - filters, findByCode, findByCodeLazy, findByPropsLazy, mapMangledModule, mapMangledModuleLazy, waitFor -} from "@webpack"; -import type Components from "discord-types/components"; -import { User } from "discord-types/general"; -import type Other from "discord-types/other"; -import type Stores from "discord-types/stores"; - -export const Margins = findByPropsLazy("marginTop20"); - -export let FluxDispatcher: Other.FluxDispatcher; -export const Flux = findByPropsLazy("connectStores"); - -export let React: typeof import("react"); -export let useState: typeof React.useState; -export let useEffect: typeof React.useEffect; -export let useMemo: typeof React.useMemo; -export let useRef: typeof React.useRef; - -export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal", "render"); - -export const RestAPI = findByPropsLazy("getAPIBaseURL", "get"); -export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); - -export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight"); - -export const MessageStore = findByPropsLazy("getRawMessages") as Omit<Stores.MessageStore, "getMessages"> & { - getMessages(chanId: string): any; -}; -export const PermissionStore = findByPropsLazy("can", "getGuildPermissions"); -export const PrivateChannelsStore = findByPropsLazy("openPrivateChannel"); -export const GuildChannelStore = findByPropsLazy("getChannels"); -export const ReadStateStore = findByPropsLazy("lastMessageId"); -export const PresenceStore = findByPropsLazy("setCurrentUserOnConnectionOpen"); -export let GuildStore: Stores.GuildStore; -export let UserStore: Stores.UserStore; -export let SelectedChannelStore: Stores.SelectedChannelStore; -export let SelectedGuildStore: any; -export let ChannelStore: Stores.ChannelStore; -export let GuildMemberStore: Stores.GuildMemberStore; -export let RelationshipStore: Stores.RelationshipStore & { - /** Get the date (as a string) that the relationship was created */ - getSince(userId: string): string; -}; - -export const Forms = {} as { - FormTitle: Components.FormTitle; - FormSection: any; - FormDivider: any; - FormText: Components.FormText; -}; -export let Card: Components.Card; -export let Button: any; -export let Switch: any; -export let Tooltip: Components.Tooltip; -export let Router: any; -export let TextInput: any; -export let Text: (props: TextProps) => JSX.Element; -export const TextArea = findByCodeLazy("handleSetRef", "textArea") as React.ComponentType<React.PropsWithRef<any>>; - -export const Select = LazyComponent(() => findByCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems")); -export const Slider = LazyComponent(() => findByCode("closestMarkerIndex", "stickToMarkers")); - -export let SnowflakeUtils: { fromTimestamp: (timestamp: number) => string, extractTimestamp: (snowflake: string) => number; }; -waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); - -export let Parser: any; -export let Alerts: { - show(alert: { - title: any; - body: React.ReactNode; - className?: string; - confirmColor?: string; - cancelText?: string; - confirmText?: string; - secondaryConfirmText?: string; - onCancel?(): void; - onConfirm?(): void; - onConfirmSecondary?(): void; - }): void; - /** This is a noop, it does nothing. */ - close(): void; -}; -const ToastType = { - MESSAGE: 0, - SUCCESS: 1, - FAILURE: 2, - CUSTOM: 3 -}; -const ToastPosition = { - TOP: 0, - BOTTOM: 1 -}; - -export const Toasts = { - Type: ToastType, - Position: ToastPosition, - // what's less likely than getting 0 from Math.random()? Getting it twice in a row - genId: () => (Math.random() || Math.random()).toString(36).slice(2), - - // hack to merge with the following interface, dunno if there's a better way - ...{} as { - show(data: { - message: string, - id: string, - /** - * Toasts.Type - */ - type: number, - options?: { - /** - * Toasts.Position - */ - position?: number; - component?: React.ReactNode, - duration?: number; - }; - }): void; - pop(): void; - } -}; - -export const UserUtils = { - fetchUser: findByCodeLazy(".USER(", "getUser") as (id: string) => Promise<User>, -}; - -export const Clipboard = mapMangledModuleLazy('document.queryCommandEnabled("copy")||document.queryCommandSupported("copy")', { - copy: filters.byCode(".default.copy("), - SUPPORTS_COPY: x => typeof x === "boolean", -}); - -export const NavigationRouter = mapMangledModuleLazy("Transitioning to external path", { - transitionTo: filters.byCode("Transitioning to external path"), - transitionToGuild: filters.byCode("transitionToGuild"), - goBack: filters.byCode("goBack()"), - goForward: filters.byCode("goForward()"), -}); - -waitFor("useState", m => { - React = m; - ({ useEffect, useState, useMemo, useRef } = React); -}); - -waitFor(["dispatch", "subscribe"], m => { - FluxDispatcher = m; - const cb = () => { - m.unsubscribe("CONNECTION_OPEN", cb); - _resolveReady(); - }; - m.subscribe("CONNECTION_OPEN", cb); -}); - -waitFor(["getCurrentUser", "initialize"], m => UserStore = m); -waitFor("getSortedPrivateChannels", m => ChannelStore = m); -waitFor("getCurrentlySelectedChannelId", m => SelectedChannelStore = m); -waitFor("getLastSelectedGuildId", m => SelectedGuildStore = m); -waitFor("getGuildCount", m => GuildStore = m); -waitFor(["getMember", "initialize"], m => GuildMemberStore = m); -waitFor("getRelationshipType", m => RelationshipStore = m); - -waitFor(["Hovers", "Looks", "Sizes"], m => Button = m); -waitFor(filters.byCode("helpdeskArticleId"), m => Switch = m); -waitFor(["Positions", "Colors"], m => Tooltip = m); -waitFor(m => m.Types?.PRIMARY === "cardPrimary", m => Card = m); - -waitFor(filters.byCode("errorSeparator"), m => Forms.FormTitle = m); -waitFor(filters.byCode("titleClassName", "sectionTitle"), m => Forms.FormSection = m); -waitFor(m => m.Types?.INPUT_PLACEHOLDER, m => Forms.FormText = m); - -waitFor(m => { - if (typeof m !== "function") return false; - const s = m.toString(); - return s.length < 200 && s.includes("().divider"); -}, m => Forms.FormDivider = m); - -// This is the same module but this is easier -waitFor(filters.byCode("currentToast?"), m => Toasts.show = m); -waitFor(filters.byCode("currentToast:null"), m => Toasts.pop = m); - -waitFor(["show", "close"], m => Alerts = m); -waitFor("parseTopic", m => Parser = m); - -waitFor(["open", "saveAccountChanges"], m => Router = m); -waitFor(["defaultProps", "Sizes", "contextType"], m => TextInput = m); - -waitFor(m => { - if (typeof m !== "function") return false; - const s = m.toString(); - return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white")); -}, m => Text = m); - -export type TextProps = React.PropsWithChildren & { - variant: TextVariant; - style?: React.CSSProperties; - color?: string; - tag?: "div" | "span" | "p" | "strong" | `h${1 | 2 | 3 | 4 | 5 | 6}`; - selectable?: boolean; - lineClamp?: number; - id?: string; - className?: string; -}; - -export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; - -type RC<C> = React.ComponentType<React.PropsWithChildren<C & Record<string, any>>>; -interface Menu { - ContextMenu: RC<{ - navId: string; - onClose(): void; - className?: string; - style?: React.CSSProperties; - hideScroller?: boolean; - onSelect?(): void; - }>; - MenuSeparator: React.ComponentType; - MenuGroup: RC<any>; - MenuItem: RC<{ - id: string; - label: string; - render?: React.ComponentType; - onChildrenScroll?: Function; - childRowHeight?: number; - listClassName?: string; - }>; - MenuCheckboxItem: RC<{ - id: string; - }>; - MenuRadioItem: RC<{ - id: string; - }>; - MenuControlItem: RC<{ - id: string; - interactive?: boolean; - }>; -} - -/** - * Discord's Context menu items. - * To use anything but Menu.ContextMenu, your plugin HAS TO - * depend on MenuItemDeobfuscatorAPI. Otherwise they will throw - */ -export const Menu = proxyLazy(() => { - const hasDeobfuscator = Vencord.Settings.plugins.MenuItemDeobfuscatorAPI.enabled; - const menuItems = ["MenuSeparator", "MenuGroup", "MenuItem", "MenuCheckboxItem", "MenuRadioItem", "MenuControlItem"]; - - const map = mapMangledModule("♫ ⊂(。◕‿‿◕。⊂) ♪", { - ContextMenu: filters.byCode("getContainerProps"), - ...Object.fromEntries((hasDeobfuscator ? menuItems : []).map(s => [s, (m: any) => m.name === s])) - }) as Menu; - - if (!hasDeobfuscator) { - for (const m of menuItems) - Object.defineProperty(map, m, { - get() { - throw new Error("MenuItemDeobfuscator must be enabled to use this."); - } - }); - } - - return map; -}); - -export const ContextMenu = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN"', { - open: filters.byCode("stopPropagation"), - openLazy: m => m.toString().length < 50, - close: filters.byCode("CONTEXT_MENU_CLOSE") -}) as { - close(): void; - open( - event: React.UIEvent, - render?: Menu["ContextMenu"], - options?: { enableSpellCheck?: boolean; }, - renderLazy?: () => Promise<Menu["ContextMenu"]> - ): void; - openLazy( - event: React.UIEvent, - renderLazy?: () => Promise<Menu["ContextMenu"]>, - options?: { enableSpellCheck?: boolean; } - ): void; -}; - -export const MaskedLinkStore = mapMangledModuleLazy('"MaskedLinkStore"', { - openUntrustedLink: filters.byCode(".apply(this,arguments)") -}); diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts new file mode 100644 index 000000000..27d103fce --- /dev/null +++ b/src/webpack/common/components.ts @@ -0,0 +1,56 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +// eslint-disable-next-line path-alias/no-relative +import { filters, findByPropsLazy } from "../webpack"; +import { waitForComponent } from "./internal"; +import * as t from "./types/components"; + +export const Forms = { + FormTitle: waitForComponent<t.FormTitle>("FormTitle", filters.byCode("errorSeparator")), + FormSection: waitForComponent<t.FormSection>("FormSection", filters.byCode("titleClassName", "sectionTitle")), + FormDivider: waitForComponent<t.FormDivider>("FormDivider", m => { + if (typeof m !== "function") return false; + const s = m.toString(); + return s.length < 200 && s.includes(".divider"); + }), + FormText: waitForComponent<t.FormText>("FormText", m => m.Types?.INPUT_PLACEHOLDER), +}; + +export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY === "cardPrimary"); +export const Button = waitForComponent<t.Button>("Button", ["Hovers", "Looks", "Sizes"]); +export const Switch = waitForComponent<t.Switch>("Switch", filters.byCode("tooltipNote", "ringTarget")); +export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", ["Positions", "Colors"]); +export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); +export const TextInput = waitForComponent<t.TextInput>("TextInput", ["defaultProps", "Sizes", "contextType"]); +export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.byCode("handleSetRef", "textArea")); +export const Text = waitForComponent<t.Text>("Text", m => { + if (typeof m !== "function") return false; + const s = m.toString(); + return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white")); +}); +export const Select = waitForComponent<t.Select>("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems")); +export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers")); +export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]); + +export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>; +/** + * @deprecated Use @utils/margins instead + */ +export const Margins: t.Margins = findByPropsLazy("marginTop20"); +export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED"); diff --git a/src/plugins/silentTyping.ts b/src/webpack/common/index.ts similarity index 61% rename from src/plugins/silentTyping.ts rename to src/webpack/common/index.ts index 8680a8667..dff7826cb 100644 --- a/src/plugins/silentTyping.ts +++ b/src/webpack/common/index.ts @@ -1,6 +1,6 @@ /* * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors + * Copyright (c) 2023 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,18 +16,12 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +export * from "./components"; +export * from "./menu"; +export * from "./react"; +export * from "./stores"; +export * as ComponentTypes from "./types/components.d"; +export * as MenuTypes from "./types/menu.d"; +export * as UtilTypes from "./types/utils.d"; +export * from "./utils"; -export default definePlugin({ - name: "SilentTyping", - authors: [Devs.Ven], - description: "Hide that you are typing", - patches: [{ - find: "startTyping:", - replacement: { - match: /startTyping:.+?,stop/, - replace: "startTyping:()=>{},stop" - } - }] -}); diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx new file mode 100644 index 000000000..e2f42d8c3 --- /dev/null +++ b/src/webpack/common/internal.tsx @@ -0,0 +1,40 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { LazyComponent } from "@utils/misc"; + +// eslint-disable-next-line path-alias/no-relative +import { FilterFn, filters, waitFor } from "../webpack"; + +export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T { + let myValue: T = function () { + throw new Error(`Vencord could not find the ${name} Component`); + } as any; + + const lazyComponent = LazyComponent(() => myValue) as T; + waitFor(filter, (v: any) => { + myValue = v; + Object.assign(lazyComponent, v); + }); + + return lazyComponent; +} + +export function waitForStore(name: string, cb: (v: any) => void) { + waitFor(filters.byStoreName(name), cb); +} diff --git a/src/webpack/common/menu.ts b/src/webpack/common/menu.ts new file mode 100644 index 000000000..6ecd754ef --- /dev/null +++ b/src/webpack/common/menu.ts @@ -0,0 +1,51 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { proxyLazy } from "@utils/proxyLazy"; + +// eslint-disable-next-line path-alias/no-relative +import { filters, mapMangledModule, mapMangledModuleLazy } from "../webpack"; +import type * as t from "./types/menu"; + +export const Menu: t.Menu = proxyLazy(() => { + const hasDeobfuscator = Vencord.Settings.plugins.MenuItemDeobfuscatorAPI.enabled; + const menuItems = ["MenuSeparator", "MenuGroup", "MenuItem", "MenuCheckboxItem", "MenuRadioItem", "MenuControlItem"]; + + const map = mapMangledModule("♫ ⊂(。◕‿‿◕。⊂) ♪", { + ContextMenu: filters.byCode("getContainerProps"), + ...Object.fromEntries((hasDeobfuscator ? menuItems : []).map(s => [s, (m: any) => m.name === s])) + }) as t.Menu; + + if (!hasDeobfuscator) { + for (const m of menuItems) + Object.defineProperty(map, m, { + get() { + throw new Error("MenuItemDeobfuscator must be enabled to use this."); + } + }); + } + + return map; +}); + +export const ContextMenu: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN"', { + open: filters.byCode("stopPropagation"), + openLazy: m => m.toString().length < 50, + close: filters.byCode("CONTEXT_MENU_CLOSE") +}); + diff --git a/src/webpack/common/react.ts b/src/webpack/common/react.ts new file mode 100644 index 000000000..d73a3dfea --- /dev/null +++ b/src/webpack/common/react.ts @@ -0,0 +1,33 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +// eslint-disable-next-line path-alias/no-relative +import { findByPropsLazy, waitFor } from "../webpack"; + +export let React: typeof import("react"); +export let useState: typeof React.useState; +export let useEffect: typeof React.useEffect; +export let useMemo: typeof React.useMemo; +export let useRef: typeof React.useRef; + +export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render"); + +waitFor("useState", m => { + React = m; + ({ useEffect, useState, useMemo, useRef } = React); +}); diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts new file mode 100644 index 000000000..0bd9e87fb --- /dev/null +++ b/src/webpack/common/stores.ts @@ -0,0 +1,89 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import type * as Stores from "discord-types/stores"; + +// eslint-disable-next-line path-alias/no-relative +import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "../webpack"; +import { waitForStore } from "./internal"; +import * as t from "./types/stores"; + +export const Flux: t.Flux = findByPropsLazy("connectStores"); + +type GenericStore = t.FluxStore & Record<string, any>; + +export let MessageStore: Omit<Stores.MessageStore, "getMessages"> & { + getMessages(chanId: string): any; +}; + +// this is not actually a FluxStore +export const PrivateChannelsStore = findByPropsLazy("openPrivateChannel"); +export let PermissionStore: GenericStore; +export let GuildChannelStore: GenericStore; +export let ReadStateStore: GenericStore; +export let PresenceStore: GenericStore; + +export let GuildStore: Stores.GuildStore & t.FluxStore; +export let UserStore: Stores.UserStore & t.FluxStore; +export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore; +export let SelectedGuildStore: t.FluxStore & Record<string, any>; +export let ChannelStore: Stores.ChannelStore & t.FluxStore; +export let GuildMemberStore: Stores.GuildMemberStore & t.FluxStore; +export let RelationshipStore: Stores.RelationshipStore & t.FluxStore & { + /** Get the date (as a string) that the relationship was created */ + getSince(userId: string): string; +}; + +export let WindowStore: t.WindowStore; + +export const MaskedLinkStore = mapMangledModuleLazy('"MaskedLinkStore"', { + openUntrustedLink: filters.byCode(".apply(this,arguments)") +}); + +/** + * React hook that returns stateful data for one or more stores + * You might need a custom comparator (4th argument) if your store data is an object + * + * @param stores The stores to listen to + * @param mapper A function that returns the data you need + * @param idk some thing, idk just pass null + * @param isEqual A custom comparator for the data returned by mapper + * + * @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id); + */ +export const useStateFromStores: <T>( + stores: t.FluxStore[], + mapper: () => T, + idk?: any, + isEqual?: (old: T, newer: T) => boolean +) => T + = findByCodeLazy("useStateFromStores"); + +waitForStore("UserStore", s => UserStore = s); +waitForStore("ChannelStore", m => ChannelStore = m); +waitForStore("SelectedChannelStore", m => SelectedChannelStore = m); +waitForStore("SelectedGuildStore", m => SelectedGuildStore = m); +waitForStore("GuildStore", m => GuildStore = m); +waitForStore("GuildMemberStore", m => GuildMemberStore = m); +waitForStore("RelationshipStore", m => RelationshipStore = m); +waitForStore("PermissionStore", m => PermissionStore = m); +waitForStore("PresenceStore", m => PresenceStore = m); +waitForStore("ReadStateStore", m => ReadStateStore = m); +waitForStore("GuildChannelStore", m => GuildChannelStore = m); +waitForStore("MessageStore", m => MessageStore = m); +waitForStore("WindowStore", m => WindowStore = m); diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts new file mode 100644 index 000000000..9cd01de2f --- /dev/null +++ b/src/webpack/common/types/components.d.ts @@ -0,0 +1,284 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import type { Moment } from "moment"; +import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; + +export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; +export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>; +export type Heading = `h${1 | 2 | 3 | 4 | 5 | 6}`; + +export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>; +export type ButtonLooks = Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>; + +export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & { + variant?: TextVariant; + tag?: "div" | "span" | "p" | "strong" | Heading; + selectable?: boolean; + lineClamp?: number; +}>; + +export type Text = ComponentType<TextProps>; + +export type FormTitle = ComponentType<HTMLProps<HTMLTitleElement> & PropsWithChildren<{ + /** default is h5 */ + tag?: Heading; + faded?: boolean; + disabled?: boolean; + required?: boolean; + error?: ReactNode; +}>>; + +export type FormSection = ComponentType<PropsWithChildren<{ + /** default is h5 */ + tag?: Heading; + className?: string; + titleClassName?: string; + titleId?: string; + title?: ReactNode; + disabled?: boolean; + htmlFor?: unknown; +}>>; + +export type FormDivider = ComponentType<{ + className?: string; + style?: CSSProperties; +}>; + + +export type FormText = ComponentType<PropsWithChildren<{ + disabled?: boolean; + selectable?: boolean; + /** defaults to FormText.Types.DEFAULT */ + type?: string; +}> & TextProps> & { Types: FormTextTypes; }; + +export type Tooltip = ComponentType<{ + text: ReactNode; + children: FunctionComponent<{ + onClick(): void; + onMouseEnter(): void; + onMouseLeave(): void; + onContextMenu(): void; + onFocus(): void; + onBlur(): void; + "aria-label"?: string; + }>; + "aria-label"?: string; + + allowOverflow?: boolean; + forceOpen?: boolean; + hide?: boolean; + hideOnClick?: boolean; + shouldShow?: boolean; + spacing?: number; + + /** Tooltip.Colors.BLACK */ + color?: string; + /** Tooltip.Positions.TOP */ + position?: string; + + tooltipClassName?: string; + tooltipContentClassName?: string; +}> & { + Positions: Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>; + Colors: Record<"BLACK" | "BRAND" | "CUSTOM" | "GREEN" | "GREY" | "PRIMARY" | "RED" | "YELLOW", string>; +}; + +export type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & { + editable?: boolean; + outline?: boolean; + /** Card.Types.PRIMARY */ + type?: string; +}>> & { + Types: Record<"BRAND" | "CUSTOM" | "DANGER" | "PRIMARY" | "SUCCESS" | "WARNING", string>; +}; + +export type Button = ComponentType<PropsWithChildren<Omit<HTMLProps<HTMLButtonElement>, "size"> & { + /** Button.Looks.FILLED */ + look?: string; + /** Button.Colors.BRAND */ + color?: string; + /** Button.Sizes.MEDIUM */ + size?: string; + /** Button.BorderColors.BLACK */ + borderColor?: string; + + wrapperClassName?: string; + className?: string; + innerClassName?: string; + + buttonRef?: Ref<HTMLButtonElement>; + focusProps?: any; + + submittingStartedLabel?: string; + submittingFinishedLabel?: string; +}>> & { + BorderColors: Record<"BLACK" | "BRAND" | "BRAND_NEW" | "GREEN" | "LINK" | "PRIMARY" | "RED" | "TRANSPARENT" | "WHITE" | "YELLOW", string>; + Colors: Record<"BRAND" | "RED" | "GREEN" | "YELLOW" | "PRIMARY" | "LINK" | "WHITE" | "BLACK" | "TRANSPARENT" | "BRAND_NEW" | "CUSTOM", string>; + Hovers: Record<"DEFAULT" | "BRAND" | "RED" | "GREEN" | "YELLOW" | "PRIMARY" | "LINK" | "WHITE" | "BLACK" | "TRANSPARENT", string>; + Looks: Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>; + Sizes: Record<"NONE" | "TINY" | "SMALL" | "MEDIUM" | "LARGE" | "XLARGE" | "MIN" | "MAX" | "ICON", string>; + + Link: any; +}; + +export type Switch = ComponentType<PropsWithChildren<{ + value: boolean; + onChange(value: boolean): void; + + disabled?: boolean; + hideBorder?: boolean; + className?: string; + style?: CSSProperties; + + note?: ReactNode; + tooltipNote?: ReactNode; +}>>; + +export type Timestamp = ComponentType<PropsWithChildren<{ + timestamp: Moment; + isEdited?: boolean; + + className?: string; + id?: string; + + cozyAlt?: boolean; + compact?: boolean; + isInline?: boolean; + isVisibleOnlyOnHover?: boolean; +}>>; + +export type TextInput = ComponentType<PropsWithChildren<{ + name?: string; + onChange?(value: string, name?: string): void; + placeholder?: string; + editable?: boolean; + maxLength?: number; + error?: string; + + inputClassName?: string; + inputPrefix?: string; + inputRef?: Ref<HTMLInputElement>; + prefixElement?: ReactNode; + + focusProps?: any; + + /** TextInput.Sizes.DEFAULT */ + size?: string; +} & Omit<HTMLProps<HTMLInputElement>, "onChange">>> & { + Sizes: Record<"DEFAULT" | "MINI", string>; +}; + +export type TextArea = ComponentType<PropsWithRef<HTMLProps<HTMLTextAreaElement>>>; + +interface SelectOption { + disabled?: boolean; + value: any; + label: string; + key?: React.Key; + default?: boolean; +} + +export type Select = ComponentType<PropsWithChildren<{ + placeholder?: string; + options: ReadonlyArray<SelectOption>; // TODO + + /** + * - 0 ~ Filled + * - 1 ~ Custom + */ + look?: 0 | 1; + className?: string; + popoutClassName?: string; + popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center"; + optionClassName?: string; + + autoFocus?: boolean; + isDisabled?: boolean; + clearable?: boolean; + closeOnSelect?: boolean; + hideIcon?: boolean; + + select(value: any): void; + isSelected(value: any): boolean; + serialize(value: any): string; + clear?(): void; + + maxVisibleItems?: number; + popoutWidth?: number; + + onClose?(): void; + onOpen?(): void; + + renderOptionLabel?(option: SelectOption): ReactNode; + /** discord stupid this gets all options instead of one yeah */ + renderOptionValue?(option: SelectOption[]): ReactNode; + + "aria-label"?: boolean; + "aria-labelledby"?: boolean; +}>>; + +export type Slider = ComponentType<PropsWithChildren<{ + initialValue: number; + defaultValue?: number; + keyboardStep?: number; + maxValue?: number; + minValue?: number; + markers?: number[]; + stickToMarkers?: boolean; + + /** 0 above, 1 below */ + markerPosition?: 0 | 1; + orientation?: "horizontal" | "vertical"; + + getAriaValueText?(currentValue: number): string; + renderMarker?(marker: number): ReactNode; + onMarkerRender?(marker: number): ReactNode; + onValueRender?(value: number): ReactNode; + onValueChange?(value: number): void; + asValueChanges?(value: number): void; + + className?: string; + disabled?: boolean; + handleSize?: number; + mini?: boolean; + hideBubble?: boolean; + + fillStyles?: CSSProperties; + barStyles?: CSSProperties; + grabberStyles?: CSSProperties; + grabberClassName?: string; + barClassName?: string; + + "aria-hidden"?: boolean; + "aria-label"?: string; + "aria-labelledby"?: string; + "aria-describedby"?: string; +}>>; + +// TODO - type maybe idk probably not that useful other than the constants +export type Flex = ComponentType<PropsWithChildren<any>> & { + Align: Record<"START" | "END" | "CENTER" | "STRETCH" | "BASELINE", string>; + Direction: Record<"VERTICAL" | "HORIZONTAL" | "HORIZONTAL_REVERSE", string>; + Justify: Record<"START" | "END" | "CENTER" | "BETWEEN" | "AROUND", string>; + Wrap: Record<"NO_WRAP" | "WRAP" | "WRAP_REVERSE", string>; + + Content: ComponentType<PropsWithChildren<any>>; + Sidebar: ComponentType<PropsWithChildren<any>>; +}; diff --git a/src/webpack/common/types/fluxEvents.d.ts b/src/webpack/common/types/fluxEvents.d.ts new file mode 100644 index 000000000..36bcb2cde --- /dev/null +++ b/src/webpack/common/types/fluxEvents.d.ts @@ -0,0 +1,40 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/* +function makeFluxEventList() { + // prefill MESSAGE_CREATE so that typescript infers this is a String Set + // without explicitly typing so that this function is also valid javascript + const events = new Set(["MESSAGE_CREATE"]); + + const { nodes } = Vencord.Webpack.Common.FluxDispatcher._actionHandlers._dependencyGraph; + for (const nodeId in nodes) { + for (const event in nodes[nodeId].actionHandler) { + events.add(event); + } + } + for (const event in Vencord.Webpack.Common.FluxDispatcher._subscriptions) { + events.add(event); + } + + return Array.from(events, e => JSON.stringify(e)).sort().join("|"); +} +*/ + +// 46kb worth of events ??????? +export type FluxEvents = "ACCESSIBILITY_COLORBLIND_TOGGLE" | "ACCESSIBILITY_DARK_SIDEBAR_TOGGLE" | "ACCESSIBILITY_DESATURATE_ROLES_TOGGLE" | "ACCESSIBILITY_DETECTION_MODAL_SEEN" | "ACCESSIBILITY_FORCED_COLORS_MODAL_SEEN" | "ACCESSIBILITY_KEYBOARD_MODE_DISABLE" | "ACCESSIBILITY_KEYBOARD_MODE_ENABLE" | "ACCESSIBILITY_LOW_CONTRAST_TOGGLE" | "ACCESSIBILITY_RESET_TO_DEFAULT" | "ACCESSIBILITY_SET_ALWAYS_SHOW_LINK_DECORATIONS" | "ACCESSIBILITY_SET_FONT_SIZE" | "ACCESSIBILITY_SET_MESSAGE_GROUP_SPACING" | "ACCESSIBILITY_SET_PREFERS_REDUCED_MOTION" | "ACCESSIBILITY_SET_ROLE_STYLE" | "ACCESSIBILITY_SET_SATURATION" | "ACCESSIBILITY_SET_SYNC_FORCED_COLORS" | "ACCESSIBILITY_SET_ZOOM" | "ACCESSIBILITY_SUBMIT_BUTTON_TOGGLE" | "ACCESSIBILITY_SUPPORT_CHANGED" | "ACCESSIBILITY_SYNC_PROFILE_THEME_WITH_USER_THEME_TOGGLE" | "ACCESSIBILITY_SYSTEM_COLOR_PREFERENCES_CHANGED" | "ACCESSIBILITY_SYSTEM_PREFERS_CONTRAST_CHANGED" | "ACCESSIBILITY_SYSTEM_PREFERS_REDUCED_MOTION_CHANGED" | "ACK_APPROVED_GUILD_JOIN_REQUEST" | "ACTIVE_CHANNELS_FETCH_FAILURE" | "ACTIVE_CHANNELS_FETCH_START" | "ACTIVE_CHANNELS_FETCH_SUCCESS" | "ACTIVE_OUTBOUND_PROMOTIONS_FETCH" | "ACTIVE_OUTBOUND_PROMOTIONS_FETCH_FAIL" | "ACTIVE_OUTBOUND_PROMOTIONS_FETCH_SUCCESS" | "ACTIVITY_INVITE_EDUCATION_DISMISS" | "ACTIVITY_INVITE_MODAL_CLOSE" | "ACTIVITY_INVITE_MODAL_OPEN" | "ACTIVITY_JOIN" | "ACTIVITY_JOIN_FAILED" | "ACTIVITY_JOIN_LOADING" | "ACTIVITY_LAUNCH_FAIL" | "ACTIVITY_METADATA_UPDATE" | "ACTIVITY_PIP_MODE_UPDATE" | "ACTIVITY_PLAY" | "ACTIVITY_SCREEN_ORIENTATION_UPDATE" | "ACTIVITY_START" | "ACTIVITY_SYNC" | "ACTIVITY_SYNC_STOP" | "ACTIVITY_UPDATE_FAIL" | "ACTIVITY_UPDATE_START" | "ACTIVITY_UPDATE_SUCCESS" | "ADD_STICKER_PREVIEW" | "ADMIN_ONBOARDING_GUIDE_HIDE" | "AFK" | "AGE_GATE_FAILURE_MODAL_OPEN" | "AGE_GATE_LOGOUT_UNDERAGE_NEW_USER" | "AGE_GATE_MODAL_CLOSE" | "AGE_GATE_MODAL_OPEN" | "AGE_GATE_SUCCESS_MODAL_OPEN" | "ALLOW_SPAM_MESSAGES_FOR_USER" | "APPLICATIONS_FETCH" | "APPLICATIONS_FETCH_FAIL" | "APPLICATIONS_FETCH_SUCCESS" | "APPLICATION_ACTIVITY_STATISTICS_FETCH_FAIL" | "APPLICATION_ACTIVITY_STATISTICS_FETCH_START" | "APPLICATION_ACTIVITY_STATISTICS_FETCH_SUCCESS" | "APPLICATION_BRANCHES_FETCH_FAIL" | "APPLICATION_BRANCHES_FETCH_SUCCESS" | "APPLICATION_BUILD_FETCH_START" | "APPLICATION_BUILD_FETCH_SUCCESS" | "APPLICATION_BUILD_NOT_FOUND" | "APPLICATION_BUILD_SIZE_FETCH_FAIL" | "APPLICATION_BUILD_SIZE_FETCH_START" | "APPLICATION_BUILD_SIZE_FETCH_SUCCESS" | "APPLICATION_COMMANDS_FETCH" | "APPLICATION_COMMANDS_FETCH_FOR_APPLICATION" | "APPLICATION_COMMAND_AUTOCOMPLETE_REQUEST" | "APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE" | "APPLICATION_COMMAND_FETCH" | "APPLICATION_COMMAND_REGISTRY_UPDATE" | "APPLICATION_COMMAND_SEARCH_STORE_QUERY" | "APPLICATION_COMMAND_SEARCH_STORE_UI_UPDATE" | "APPLICATION_COMMAND_SEARCH_STORE_UPDATE" | "APPLICATION_COMMAND_SET_ACTIVE_COMMAND" | "APPLICATION_COMMAND_SET_PREFERRED_COMMAND" | "APPLICATION_COMMAND_UPDATE_CHANNEL_STATE" | "APPLICATION_COMMAND_UPDATE_OPTIONS" | "APPLICATION_COMMAND_USED" | "APPLICATION_FETCH" | "APPLICATION_FETCH_FAIL" | "APPLICATION_FETCH_SUCCESS" | "APPLICATION_STORE_ACCEPT_EULA" | "APPLICATION_STORE_ACCEPT_STORE_TERMS" | "APPLICATION_STORE_CLEAR_DATA" | "APPLICATION_STORE_DIRECTORY_FETCH_SUCCESS" | "APPLICATION_STORE_DIRECTORY_MUTE" | "APPLICATION_STORE_DIRECTORY_UNMUTE" | "APPLICATION_STORE_LOCATION_CHANGE" | "APPLICATION_STORE_MATURE_AGREE" | "APPLICATION_STORE_RESET_NAVIGATION" | "APPLICATION_SUBSCRIPTIONS_CHANNEL_NOTICE_DISMISSED" | "APPLICATION_SUBSCRIPTIONS_FETCH_ENTITLEMENTS" | "APPLICATION_SUBSCRIPTIONS_FETCH_ENTITLEMENTS_FAILURE" | "APPLICATION_SUBSCRIPTIONS_FETCH_ENTITLEMENTS_SUCCESS" | "APPLICATION_SUBSCRIPTIONS_FETCH_LISTINGS" | "APPLICATION_SUBSCRIPTIONS_FETCH_LISTINGS_FAILURE" | "APPLICATION_SUBSCRIPTIONS_FETCH_LISTINGS_SUCCESS" | "APPLICATION_SUBSCRIPTIONS_FETCH_LISTING_FOR_PLAN_SUCCESS" | "APPLIED_BOOSTS_COOLDOWN_FETCH_SUCCESS" | "APPLIED_GUILD_BOOST_COUNT_UPDATE" | "APP_STATE_UPDATE" | "APP_VIEW_SET_HOME_LINK" | "AUDIO_INPUT_DETECTED" | "AUDIO_RESET" | "AUDIO_SET_ATTENUATION" | "AUDIO_SET_AUTOMATIC_GAIN_CONTROL" | "AUDIO_SET_DEBUG_LOGGING" | "AUDIO_SET_DISPLAY_SILENCE_WARNING" | "AUDIO_SET_ECHO_CANCELLATION" | "AUDIO_SET_INPUT_DEVICE" | "AUDIO_SET_INPUT_VOLUME" | "AUDIO_SET_LOCAL_PAN" | "AUDIO_SET_LOCAL_VIDEO_DISABLED" | "AUDIO_SET_LOCAL_VOLUME" | "AUDIO_SET_LOOPBACK" | "AUDIO_SET_MODE" | "AUDIO_SET_NOISE_CANCELLATION" | "AUDIO_SET_NOISE_SUPPRESSION" | "AUDIO_SET_OUTPUT_DEVICE" | "AUDIO_SET_OUTPUT_VOLUME" | "AUDIO_SET_QOS" | "AUDIO_SET_SUBSYSTEM" | "AUDIO_SET_TEMPORARY_SELF_MUTE" | "AUDIO_TOGGLE_LOCAL_MUTE" | "AUDIO_TOGGLE_SELF_DEAF" | "AUDIO_TOGGLE_SELF_MUTE" | "AUDIO_VOLUME_CHANGE" | "AUDIT_LOG_FETCH_FAIL" | "AUDIT_LOG_FETCH_NEXT_PAGE_FAIL" | "AUDIT_LOG_FETCH_NEXT_PAGE_START" | "AUDIT_LOG_FETCH_NEXT_PAGE_SUCCESS" | "AUDIT_LOG_FETCH_START" | "AUDIT_LOG_FETCH_SUCCESS" | "AUDIT_LOG_FILTER_BY_ACTION" | "AUDIT_LOG_FILTER_BY_USER" | "AUTH_INVITE_UPDATE" | "AUTH_SESSION_CHANGE" | "AUTO_MODERATION_MENTION_RAID_DETECTION" | "AUTO_MODERATION_MENTION_RAID_NOTICE_DISMISS" | "BILLING_IP_COUNTRY_CODE_FAILURE" | "BILLING_IP_COUNTRY_CODE_FETCH_START" | "BILLING_LOCALIZED_PRICING_PROMO_FAILURE" | "BILLING_MOST_RECENT_SUBSCRIPTION_FETCH_SUCCESS" | "BILLING_PAYMENTS_FETCH_SUCCESS" | "BILLING_PAYMENT_FETCH_SUCCESS" | "BILLING_PAYMENT_SOURCES_FETCH_FAIL" | "BILLING_PAYMENT_SOURCES_FETCH_START" | "BILLING_PAYMENT_SOURCES_FETCH_SUCCESS" | "BILLING_PAYMENT_SOURCE_CREATE_FAIL" | "BILLING_PAYMENT_SOURCE_CREATE_START" | "BILLING_PAYMENT_SOURCE_CREATE_SUCCESS" | "BILLING_PAYMENT_SOURCE_REMOVE_CLEAR_ERROR" | "BILLING_PAYMENT_SOURCE_REMOVE_FAIL" | "BILLING_PAYMENT_SOURCE_REMOVE_START" | "BILLING_PAYMENT_SOURCE_REMOVE_SUCCESS" | "BILLING_PAYMENT_SOURCE_UPDATE_CLEAR_ERROR" | "BILLING_PAYMENT_SOURCE_UPDATE_FAIL" | "BILLING_PAYMENT_SOURCE_UPDATE_START" | "BILLING_PAYMENT_SOURCE_UPDATE_SUCCESS" | "BILLING_POPUP_BRIDGE_CALLBACK" | "BILLING_POPUP_BRIDGE_STATE_UPDATE" | "BILLING_PURCHASE_TOKEN_AUTH_CLEAR_STATE" | "BILLING_SET_IP_COUNTRY_CODE" | "BILLING_SET_LOCALIZED_PRICING_PROMO" | "BILLING_SUBSCRIPTION_CANCEL_FAIL" | "BILLING_SUBSCRIPTION_CANCEL_START" | "BILLING_SUBSCRIPTION_CANCEL_SUCCESS" | "BILLING_SUBSCRIPTION_FETCH_FAIL" | "BILLING_SUBSCRIPTION_FETCH_START" | "BILLING_SUBSCRIPTION_FETCH_SUCCESS" | "BILLING_SUBSCRIPTION_RESET" | "BILLING_SUBSCRIPTION_UPDATE_FAIL" | "BILLING_SUBSCRIPTION_UPDATE_START" | "BILLING_SUBSCRIPTION_UPDATE_SUCCESS" | "BILLING_USER_PREMIUM_LIKELIHOOD_FETCH" | "BILLING_USER_PREMIUM_LIKELIHOOD_FETCH_ERROR" | "BILLING_USER_PREMIUM_LIKELIHOOD_FETCH_SUCCESS" | "BILLING_USER_TRIAL_OFFER_ACKNOWLEDGED_SUCCESS" | "BILLING_USER_TRIAL_OFFER_FETCH_SUCCESS" | "BLOCKED_DOMAIN_LIST_FETCHED" | "BOOSTED_GUILD_GRACE_PERIOD_NOTICE_DISMISS" | "BRAINTREE_CREATE_CLIENT_SUCCESS" | "BRAINTREE_CREATE_PAYPAL_CLIENT_SUCCESS" | "BRAINTREE_CREATE_VENMO_CLIENT_SUCCESS" | "BRAINTREE_TEARDOWN_PAYPAL_CLIENT" | "BRAINTREE_TEARDOWN_VENMO_CLIENT" | "BRAINTREE_TOKENIZE_PAYPAL_START" | "BRAINTREE_TOKENIZE_PAYPAL_SUCCESS" | "BRAINTREE_TOKENIZE_VENMO_START" | "BRAINTREE_TOKENIZE_VENMO_SUCCESS" | "BROWSER_HANDOFF_BEGIN" | "BROWSER_HANDOFF_FROM_APP" | "BROWSER_HANDOFF_SET_USER" | "BROWSER_HANDOFF_UNAVAILABLE" | "BUILD_OVERRIDE_RESOLVED" | "BULK_ACK" | "BULK_CLEAR_RECENTS" | "BURST_REACTION_ADD" | "BURST_REACTION_ANIMATION_ADD" | "BURST_REACTION_EFFECT_CLEAR" | "BURST_REACTION_EFFECT_PLAY" | "BURST_REACTION_REMOVE" | "CACHE_LOADED" | "CACHE_LOADED_LAZY" | "CALL_CHAT_TOASTS_SET_ENABLED" | "CALL_CONNECT" | "CALL_CREATE" | "CALL_DELETE" | "CALL_ENQUEUE_RING" | "CALL_UPDATE" | "CANDIDATE_GAMES_CHANGE" | "CATEGORY_COLLAPSE" | "CATEGORY_COLLAPSE_ALL" | "CATEGORY_EXPAND" | "CATEGORY_EXPAND_ALL" | "CERTIFIED_DEVICES_SET" | "CHANGE_LOG_CLOSE" | "CHANGE_LOG_FETCH_SUCCESS" | "CHANGE_LOG_LOCK" | "CHANGE_LOG_OPEN" | "CHANGE_LOG_SET_OVERRIDE" | "CHANGE_LOG_UNLOCK" | "CHANNEL_ACK" | "CHANNEL_CALL_POPOUT_WINDOW_OPEN" | "CHANNEL_COLLAPSE" | "CHANNEL_CREATE" | "CHANNEL_DELETE" | "CHANNEL_FOLLOWER_CREATED" | "CHANNEL_FOLLOWER_STATS_FETCH_FAILURE" | "CHANNEL_FOLLOWER_STATS_FETCH_SUCCESS" | "CHANNEL_FOLLOWING_PUBLISH_BUMP_DISMISSED" | "CHANNEL_FOLLOWING_PUBLISH_BUMP_HIDE_PERMANENTLY" | "CHANNEL_HIGHLIGHTS_FETCH_START" | "CHANNEL_HIGHLIGHTS_FETCH_SUCCESS" | "CHANNEL_LOCAL_ACK" | "CHANNEL_MESSAGE_PREVIEW_LOAD_MESSAGES" | "CHANNEL_PINS_ACK" | "CHANNEL_PINS_UPDATE" | "CHANNEL_PRELOAD" | "CHANNEL_RECIPIENT_ADD" | "CHANNEL_RECIPIENT_REMOVE" | "CHANNEL_RTC_ACTIVE_CHANNELS" | "CHANNEL_RTC_SELECT_PARTICIPANT" | "CHANNEL_RTC_UPDATE_CHAT_OPEN" | "CHANNEL_RTC_UPDATE_LAYOUT" | "CHANNEL_RTC_UPDATE_PARTICIPANTS_OPEN" | "CHANNEL_RTC_UPDATE_STAGE_MUSIC_MUTED" | "CHANNEL_RTC_UPDATE_STAGE_STREAM_SIZE" | "CHANNEL_RTC_UPDATE_STAGE_VIDEO_LIMIT_BOOST_UPSELL_DISMISSED" | "CHANNEL_RTC_UPDATE_VOICE_PARTICIPANTS_HIDDEN" | "CHANNEL_SELECT" | "CHANNEL_SETTINGS_CLOSE" | "CHANNEL_SETTINGS_INIT" | "CHANNEL_SETTINGS_LOADED_INVITES" | "CHANNEL_SETTINGS_OPEN" | "CHANNEL_SETTINGS_OVERWRITE_SELECT" | "CHANNEL_SETTINGS_PERMISSIONS_INIT" | "CHANNEL_SETTINGS_PERMISSIONS_SAVE_SUCCESS" | "CHANNEL_SETTINGS_PERMISSIONS_SELECT_PERMISSION" | "CHANNEL_SETTINGS_PERMISSIONS_SET_ADVANCED_MODE" | "CHANNEL_SETTINGS_PERMISSIONS_SUBMITTING" | "CHANNEL_SETTINGS_PERMISSIONS_UPDATE_PERMISSION" | "CHANNEL_SETTINGS_SET_SECTION" | "CHANNEL_SETTINGS_SUBMIT" | "CHANNEL_SETTINGS_SUBMIT_FAILURE" | "CHANNEL_SETTINGS_SUBMIT_SUCCESS" | "CHANNEL_SETTINGS_UPDATE" | "CHANNEL_TOGGLE_MEMBERS_SECTION" | "CHANNEL_UPDATES" | "CHECKING_FOR_UPDATES" | "CHECK_LAUNCHABLE_GAME" | "CLEAR_AUTHENTICATION_ERRORS" | "CLEAR_CACHES" | "CLEAR_GUILD_CACHE" | "CLEAR_INTERACTION_MODAL_STATE" | "CLEAR_MESSAGES" | "CLEAR_OLDEST_UNREAD_MESSAGE" | "CLEAR_PENDING_CHANNEL_AND_ROLE_UPDATES" | "CLEAR_REMOTE_DISCONNECT_VOICE_CHANNEL_ID" | "CLEAR_STICKER_PREVIEW" | "CLIENT_THEMES_EDITOR_CLOSE" | "CLIENT_THEMES_EDITOR_OPEN" | "CLIPS_CLEAR_CLIPS_SESSION" | "CLIPS_SAVE_CLIP" | "CLIPS_SETTINGS_UPDATE" | "COMMANDS_MIGRATION_NOTICE_DISMISSED" | "COMMANDS_MIGRATION_OVERVIEW_TOOLTIP_DISMISSED" | "COMMANDS_MIGRATION_TOGGLE_TOOLTIP_DISMISSED" | "COMMANDS_MIGRATION_UPDATE_SUCCESS" | "CONNECTED_DEVICE_IGNORE" | "CONNECTED_DEVICE_NEVER_SHOW_MODAL" | "CONNECTED_DEVICE_SET" | "CONNECTIONS_GRID_MODAL_HIDE" | "CONNECTIONS_GRID_MODAL_SHOW" | "CONNECTION_CLOSED" | "CONNECTION_OPEN" | "CONNECTION_OPEN_SUPPLEMENTAL" | "CONNECTION_RESUMED" | "CONSOLE_COMMAND_UPDATE" | "CONTEXT_MENU_CLOSE" | "CONTEXT_MENU_OPEN" | "CREATE_PENDING_REPLY" | "CURRENT_BUILD_OVERRIDE_RESOLVED" | "CURRENT_USER_UPDATE" | "DECAY_READ_STATES" | "DELETED_ENTITY_IDS" | "DELETE_PENDING_REPLY" | "DETECTED_OFF_PLATFORM_PREMIUM_PERKS_DISMISS" | "DEVELOPER_ACTIVITY_SHELF_FETCH_FAIL" | "DEVELOPER_ACTIVITY_SHELF_FETCH_START" | "DEVELOPER_ACTIVITY_SHELF_FETCH_SUCCESS" | "DEVELOPER_ACTIVITY_SHELF_MARK_ACTIVITY_USED" | "DEVELOPER_ACTIVITY_SHELF_SET_ACTIVITY_URL_OVERRIDE" | "DEVELOPER_ACTIVITY_SHELF_TOGGLE_ENABLED" | "DEVELOPER_ACTIVITY_SHELF_TOGGLE_USE_ACTIVITY_URL_OVERRIDE" | "DEVELOPER_OPTIONS_UPDATE_SETTINGS" | "DEVELOPER_TEST_MODE_AUTHORIZATION_FAIL" | "DEVELOPER_TEST_MODE_AUTHORIZATION_START" | "DEVELOPER_TEST_MODE_AUTHORIZATION_SUCCESS" | "DEVELOPER_TEST_MODE_RESET" | "DEVELOPER_TEST_MODE_RESET_ERROR" | "DEV_TOOLS_SETTINGS_UPDATE" | "DISABLE_AUTOMATIC_ACK" | "DISCOVER_CHECKLIST_FETCH_FAILURE" | "DISCOVER_CHECKLIST_FETCH_START" | "DISCOVER_CHECKLIST_FETCH_SUCCESS" | "DISCOVER_GUILDS_FETCH_FAILURE" | "DISCOVER_GUILDS_FETCH_START" | "DISCOVER_GUILDS_FETCH_SUCCESS" | "DISMISS_FAVORITE_SUGGESTION" | "DISMISS_NITRODUCTION_TOOLTIP" | "DISPATCH_APPLICATION_ADD_TO_INSTALLATIONS" | "DISPATCH_APPLICATION_CANCEL" | "DISPATCH_APPLICATION_ERROR" | "DISPATCH_APPLICATION_INSTALL" | "DISPATCH_APPLICATION_INSTALL_SCRIPTS_PROGRESS_UPDATE" | "DISPATCH_APPLICATION_LAUNCH_SETUP_COMPLETE" | "DISPATCH_APPLICATION_LAUNCH_SETUP_START" | "DISPATCH_APPLICATION_MOVE_UP" | "DISPATCH_APPLICATION_REMOVE_FINISHED" | "DISPATCH_APPLICATION_REPAIR" | "DISPATCH_APPLICATION_STATE_UPDATE" | "DISPATCH_APPLICATION_UNINSTALL" | "DISPATCH_APPLICATION_UPDATE" | "DISPLAYED_INVITE_SHOW" | "DOMAIN_MIGRATION_FAILURE" | "DOMAIN_MIGRATION_SKIP" | "DOMAIN_MIGRATION_START" | "DRAFT_CHANGE" | "DRAFT_CLEAR" | "DRAFT_SAVE" | "DRAWER_CLOSE" | "DRAWER_OPEN" | "DRAWER_SELECT_TAB" | "DROPS_ELIGIBILITY_FETCH_SUCCESS" | "DROPS_ENROLLED_USER_FETCH_SUCCESS" | "DROPS_HEARTBEAT_FAILURE" | "DROPS_HEARTBEAT_SUCCESS" | "DROPS_PLATFORM_AVAILABILITY_SUCCESS" | "DROPS_USER_STATUS_FETCH_SUCCESS" | "EMAIL_SETTINGS_FETCH_SUCCESS" | "EMAIL_SETTINGS_UPDATE" | "EMAIL_SETTINGS_UPDATE_SUCCESS" | "EMBEDDED_ACTIVITY_CLOSE" | "EMBEDDED_ACTIVITY_DEFERRED_OPEN" | "EMBEDDED_ACTIVITY_DISCONNECT" | "EMBEDDED_ACTIVITY_DISMISS_FREE_INDICATOR" | "EMBEDDED_ACTIVITY_FETCH_SHELF" | "EMBEDDED_ACTIVITY_FETCH_SHELF_FAIL" | "EMBEDDED_ACTIVITY_FETCH_SHELF_SUCCESS" | "EMBEDDED_ACTIVITY_FREE_ACTIVITY_UPDATE" | "EMBEDDED_ACTIVITY_INBOUND_UPDATE" | "EMBEDDED_ACTIVITY_LAUNCH_FAIL" | "EMBEDDED_ACTIVITY_LAUNCH_START" | "EMBEDDED_ACTIVITY_LAUNCH_SUCCESS" | "EMBEDDED_ACTIVITY_OPEN" | "EMBEDDED_ACTIVITY_SET_CONFIG" | "EMBEDDED_ACTIVITY_SET_ORIENTATION_LOCK_STATE" | "EMOJI_DELETE" | "EMOJI_FETCH_FAILURE" | "EMOJI_FETCH_SUCCESS" | "EMOJI_TRACK_USAGE" | "EMOJI_UPLOAD_START" | "EMOJI_UPLOAD_STOP" | "ENABLE_AUTOMATIC_ACK" | "ENTITLEMENTS_FETCH_FOR_USER_SUCCESS" | "ENTITLEMENTS_GIFTABLE_FETCH_SUCCESS" | "ENTITLEMENT_CREATE" | "ENTITLEMENT_DELETE" | "ENTITLEMENT_FETCH_APPLICATION_FAIL" | "ENTITLEMENT_FETCH_APPLICATION_START" | "ENTITLEMENT_FETCH_APPLICATION_SUCCESS" | "ENTITLEMENT_UPDATE" | "EVENT_DIRECTORY_FETCH_FAILURE" | "EVENT_DIRECTORY_FETCH_START" | "EVENT_DIRECTORY_FETCH_SUCCESS" | "EXPERIMENTS_FETCH" | "EXPERIMENTS_FETCH_FAILURE" | "EXPERIMENTS_FETCH_SUCCESS" | "EXPERIMENT_OVERRIDE_BUCKET" | "EXPERIMENT_REGISTER_LEGACY" | "EXPERIMENT_TRIGGER" | "FETCH_AUTH_SESSIONS_SUCCESS" | "FETCH_GUILD_EVENTS_FOR_GUILD" | "FINGERPRINT" | "FORCE_INVISIBLE" | "FORGOT_PASSWORD_REQUEST" | "FORGOT_PASSWORD_SENT" | "FORUM_SEARCH_CLEAR" | "FORUM_SEARCH_FAILURE" | "FORUM_SEARCH_QUERY_UPDATED" | "FORUM_SEARCH_START" | "FORUM_SEARCH_SUCCESS" | "FORUM_UNREADS" | "FRIENDS_SET_INITIAL_SECTION" | "FRIENDS_SET_SECTION" | "FRIEND_INVITES_FETCH_REQUEST" | "FRIEND_INVITES_FETCH_RESPONSE" | "FRIEND_INVITE_CREATE_FAILURE" | "FRIEND_INVITE_CREATE_REQUEST" | "FRIEND_INVITE_CREATE_SUCCESS" | "FRIEND_INVITE_REVOKE_REQUEST" | "FRIEND_INVITE_REVOKE_SUCCESS" | "FRIEND_SUGGESTION_CREATE" | "FRIEND_SUGGESTION_DELETE" | "GAMES_DATABASE_FETCH" | "GAMES_DATABASE_FETCH_FAIL" | "GAMES_DATABASE_UPDATE" | "GAME_CLOUD_SYNC_COMPLETE" | "GAME_CLOUD_SYNC_CONFLICT" | "GAME_CLOUD_SYNC_ERROR" | "GAME_CLOUD_SYNC_START" | "GAME_CLOUD_SYNC_UPDATE" | "GAME_CONSOLE_FETCH_DEVICES_FAIL" | "GAME_CONSOLE_FETCH_DEVICES_START" | "GAME_CONSOLE_FETCH_DEVICES_SUCCESS" | "GAME_ICON_UPDATE" | "GAME_LAUNCHABLE_UPDATE" | "GAME_LAUNCH_FAIL" | "GAME_LAUNCH_START" | "GAME_LAUNCH_SUCCESS" | "GENERIC_PUSH_NOTIFICATION_SENT" | "GIFT_CODES_FETCH" | "GIFT_CODES_FETCH_FAILURE" | "GIFT_CODES_FETCH_SUCCESS" | "GIFT_CODE_CREATE" | "GIFT_CODE_CREATE_SUCCESS" | "GIFT_CODE_REDEEM" | "GIFT_CODE_REDEEM_FAILURE" | "GIFT_CODE_REDEEM_SUCCESS" | "GIFT_CODE_RESOLVE" | "GIFT_CODE_RESOLVE_FAILURE" | "GIFT_CODE_RESOLVE_SUCCESS" | "GIFT_CODE_REVOKE_SUCCESS" | "GIFT_CODE_UPDATE" | "GIF_PICKER_INITIALIZE" | "GIF_PICKER_QUERY" | "GIF_PICKER_QUERY_FAILURE" | "GIF_PICKER_QUERY_SUCCESS" | "GIF_PICKER_SUGGESTIONS_SUCCESS" | "GIF_PICKER_TRENDING_FETCH_SUCCESS" | "GIF_PICKER_TRENDING_SEARCH_TERMS_SUCCESS" | "GUILD_ACK" | "GUILD_ANALYTICS_ENGAGEMENT_OVERVIEW_FETCH_FAILURE" | "GUILD_ANALYTICS_ENGAGEMENT_OVERVIEW_FETCH_SUCCESS" | "GUILD_ANALYTICS_GROWTH_ACTIVATION_OVERVIEW_FETCH_FAILURE" | "GUILD_ANALYTICS_GROWTH_ACTIVATION_OVERVIEW_FETCH_SUCCESS" | "GUILD_ANALYTICS_GROWTH_ACTIVATION_RETENTION_FETCH_FAILURE" | "GUILD_ANALYTICS_GROWTH_ACTIVATION_RETENTION_FETCH_SUCCESS" | "GUILD_ANALYTICS_MEMBER_INSIGHTS_FETCH_SUCCESS" | "GUILD_APPLICATIONS_FETCH_SUCCESS" | "GUILD_APPLICATION_COMMAND_INDEX_UPDATE" | "GUILD_APPLIED_BOOSTS_FETCH_SUCCESS" | "GUILD_APPLY_BOOST_FAIL" | "GUILD_APPLY_BOOST_START" | "GUILD_APPLY_BOOST_SUCCESS" | "GUILD_BAN_ADD" | "GUILD_BAN_REMOVE" | "GUILD_BOOST_SLOTS_FETCH_SUCCESS" | "GUILD_BOOST_SLOT_CREATE" | "GUILD_BOOST_SLOT_UPDATE" | "GUILD_BOOST_SLOT_UPDATE_SUCCESS" | "GUILD_CREATE" | "GUILD_DELETE" | "GUILD_DIRECTORY_ADMIN_ENTRIES_FETCH_SUCCESS" | "GUILD_DIRECTORY_CACHED_SEARCH" | "GUILD_DIRECTORY_CATEGORY_SELECT" | "GUILD_DIRECTORY_COUNTS_FETCH_SUCCESS" | "GUILD_DIRECTORY_ENTRY_CREATE" | "GUILD_DIRECTORY_ENTRY_DELETE" | "GUILD_DIRECTORY_ENTRY_UPDATE" | "GUILD_DIRECTORY_FETCH_FAILURE" | "GUILD_DIRECTORY_FETCH_START" | "GUILD_DIRECTORY_FETCH_SUCCESS" | "GUILD_DIRECTORY_SEARCH_CLEAR" | "GUILD_DIRECTORY_SEARCH_FAILURE" | "GUILD_DIRECTORY_SEARCH_START" | "GUILD_DIRECTORY_SEARCH_SUCCESS" | "GUILD_DISCOVERY_CATEGORY_ADD" | "GUILD_DISCOVERY_CATEGORY_DELETE" | "GUILD_DISCOVERY_CATEGORY_FETCH_SUCCESS" | "GUILD_DISCOVERY_CATEGORY_UPDATE_FAIL" | "GUILD_DISCOVERY_CLEAR_SEARCH" | "GUILD_DISCOVERY_CLEAR_SEEN_GUILDS" | "GUILD_DISCOVERY_FETCH_FAILURE" | "GUILD_DISCOVERY_FETCH_START" | "GUILD_DISCOVERY_FETCH_SUCCESS" | "GUILD_DISCOVERY_GUILD_SEEN" | "GUILD_DISCOVERY_METADATA_FETCH_FAIL" | "GUILD_DISCOVERY_POPULAR_FETCH_FAILURE" | "GUILD_DISCOVERY_POPULAR_FETCH_START" | "GUILD_DISCOVERY_POPULAR_FETCH_SUCCESS" | "GUILD_DISCOVERY_SEARCH_COUNTS_FAIL" | "GUILD_DISCOVERY_SEARCH_FETCH_FAILURE" | "GUILD_DISCOVERY_SEARCH_FETCH_START" | "GUILD_DISCOVERY_SEARCH_FETCH_SUCCESS" | "GUILD_DISCOVERY_SEARCH_INIT" | "GUILD_DISCOVERY_SEARCH_UPDATE_COUNTS" | "GUILD_DISCOVERY_SELECT_CATEGORY" | "GUILD_DISCOVERY_SLUG_FETCH_SUCCESS" | "GUILD_EMOJIS_UPDATE" | "GUILD_FEATURE_ACK" | "GUILD_FEED_FEATURED_ITEMS_FETCH_FAILURE" | "GUILD_FEED_FEATURED_ITEMS_FETCH_SUCCESS" | "GUILD_FEED_FEATURE_ITEM" | "GUILD_FEED_FETCH_FAILURE" | "GUILD_FEED_FETCH_FRESH_START" | "GUILD_FEED_FETCH_PAGE_START" | "GUILD_FEED_FETCH_SUCCESS" | "GUILD_FEED_ITEM_HIDE" | "GUILD_FEED_ITEM_READ_ACK" | "GUILD_FEED_ITEM_REMOVE" | "GUILD_FEED_ITEM_UNHIDE" | "GUILD_FEED_UNFEATURE_ITEM" | "GUILD_FOLDER_COLLAPSE" | "GUILD_HOME_ENSURE_HOME_SESSION" | "GUILD_HOME_SET_SCROLL_POSITION" | "GUILD_HOME_SET_SOURCE" | "GUILD_IDENTITY_SETTINGS_CLEAR_ERRORS" | "GUILD_IDENTITY_SETTINGS_CLOSE" | "GUILD_IDENTITY_SETTINGS_INIT" | "GUILD_IDENTITY_SETTINGS_RESET_ALL_PENDING" | "GUILD_IDENTITY_SETTINGS_RESET_AND_CLOSE_FORM" | "GUILD_IDENTITY_SETTINGS_RESET_PENDING_MEMBER_CHANGES" | "GUILD_IDENTITY_SETTINGS_RESET_PENDING_PROFILE_CHANGES" | "GUILD_IDENTITY_SETTINGS_SET_DISABLE_SUBMIT" | "GUILD_IDENTITY_SETTINGS_SET_GUILD" | "GUILD_IDENTITY_SETTINGS_SET_PENDING_AVATAR" | "GUILD_IDENTITY_SETTINGS_SET_PENDING_BANNER" | "GUILD_IDENTITY_SETTINGS_SET_PENDING_BIO" | "GUILD_IDENTITY_SETTINGS_SET_PENDING_NICKNAME" | "GUILD_IDENTITY_SETTINGS_SET_PENDING_PRONOUNS" | "GUILD_IDENTITY_SETTINGS_SET_PENDING_THEME_COLORS" | "GUILD_IDENTITY_SETTINGS_SUBMIT" | "GUILD_IDENTITY_SETTINGS_SUBMIT_FAILURE" | "GUILD_IDENTITY_SETTINGS_SUBMIT_SUCCESS" | "GUILD_INTEGRATIONS_UPDATE" | "GUILD_JOIN" | "GUILD_JOIN_REQUESTS_BULK_ACTION" | "GUILD_JOIN_REQUESTS_FETCH_FAILURE" | "GUILD_JOIN_REQUESTS_FETCH_START" | "GUILD_JOIN_REQUESTS_FETCH_SUCCESS" | "GUILD_JOIN_REQUESTS_SET_APPLICATION_STATUS" | "GUILD_JOIN_REQUESTS_SET_SELECTED" | "GUILD_JOIN_REQUESTS_SET_SORT_ORDER" | "GUILD_JOIN_REQUEST_CREATE" | "GUILD_JOIN_REQUEST_DELETE" | "GUILD_JOIN_REQUEST_UPDATE" | "GUILD_MEMBERS_CHUNK" | "GUILD_MEMBERS_REQUEST" | "GUILD_MEMBER_ADD" | "GUILD_MEMBER_LIST_UPDATE" | "GUILD_MEMBER_PROFILE_UPDATE" | "GUILD_MEMBER_REMOVE" | "GUILD_MEMBER_UPDATE" | "GUILD_MEMBER_UPDATE_LOCAL" | "GUILD_MOVE" | "GUILD_MOVE_BY_ID" | "GUILD_NSFW_AGREE" | "GUILD_ONBOARDING_COMPLETE" | "GUILD_ONBOARDING_PROMPTS_FETCH_FAILURE" | "GUILD_ONBOARDING_PROMPTS_FETCH_START" | "GUILD_ONBOARDING_PROMPTS_FETCH_SUCCESS" | "GUILD_ONBOARDING_PROMPTS_LOCAL_UPDATE" | "GUILD_ONBOARDING_SELECT_OPTION" | "GUILD_ONBOARDING_SET_STEP" | "GUILD_ONBOARDING_START" | "GUILD_ONBOARDING_UPDATE_RESPONSES_SUCCESS" | "GUILD_POPOUT_FETCH_FAILURE" | "GUILD_POPOUT_FETCH_START" | "GUILD_POPOUT_FETCH_SUCCESS" | "GUILD_PROGRESS_COMPLETED_SEEN" | "GUILD_PROGRESS_DISMISS" | "GUILD_PROGRESS_INITIALIZE" | "GUILD_PROMPT_VIEWED" | "GUILD_RECOMMENDATION_FETCH" | "GUILD_RECOMMENDATION_FETCH_FAILURE" | "GUILD_RECOMMENDATION_FETCH_SUCCESS" | "GUILD_ROLE_CONNECTIONS_CONFIGURATIONS_FETCH_SUCCESS" | "GUILD_ROLE_CONNECTION_ELIGIBILITY_FETCH_SUCCESS" | "GUILD_ROLE_CREATE" | "GUILD_ROLE_DELETE" | "GUILD_ROLE_MEMBER_ADD" | "GUILD_ROLE_MEMBER_BULK_ADD" | "GUILD_ROLE_MEMBER_COUNT_FETCH_SUCCESS" | "GUILD_ROLE_MEMBER_COUNT_UPDATE" | "GUILD_ROLE_MEMBER_REMOVE" | "GUILD_ROLE_SUBSCRIPTIONS_CREATE_LISTING" | "GUILD_ROLE_SUBSCRIPTIONS_DELETE_GROUP_LISTING" | "GUILD_ROLE_SUBSCRIPTIONS_DELETE_LISTING" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTINGS" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTINGS_FAILURE" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTINGS_SUCCESS" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTING_FOR_PLAN" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_LISTING_FOR_PLAN_SUCCESS" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_RESTRICTIONS" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_RESTRICTIONS_ABORTED" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_RESTRICTIONS_FAILURE" | "GUILD_ROLE_SUBSCRIPTIONS_FETCH_RESTRICTIONS_SUCCESS" | "GUILD_ROLE_SUBSCRIPTIONS_UPDATE_GROUP_LISTING" | "GUILD_ROLE_SUBSCRIPTIONS_UPDATE_LISTING" | "GUILD_ROLE_SUBSCRIPTIONS_UPDATE_SUBSCRIPTIONS_SETTINGS" | "GUILD_ROLE_SUBSCRIPTIONS_UPDATE_SUBSCRIPTION_TRIAL" | "GUILD_ROLE_UPDATE" | "GUILD_SCHEDULED_EVENT_CREATE" | "GUILD_SCHEDULED_EVENT_DELETE" | "GUILD_SCHEDULED_EVENT_RSVPS_FETCH_SUCESS" | "GUILD_SCHEDULED_EVENT_UPDATE" | "GUILD_SCHEDULED_EVENT_USERS_FETCH_SUCCESS" | "GUILD_SCHEDULED_EVENT_USER_ADD" | "GUILD_SCHEDULED_EVENT_USER_REMOVE" | "GUILD_SETTINGS_CANCEL_CHANGES" | "GUILD_SETTINGS_CLOSE" | "GUILD_SETTINGS_DEFAULT_CHANNELS_RESET" | "GUILD_SETTINGS_DEFAULT_CHANNELS_SAVE_FAILED" | "GUILD_SETTINGS_DEFAULT_CHANNELS_SAVE_SUCCESS" | "GUILD_SETTINGS_DEFAULT_CHANNELS_SUBMIT" | "GUILD_SETTINGS_DEFAULT_CHANNELS_TOGGLE" | "GUILD_SETTINGS_INIT" | "GUILD_SETTINGS_LOADED_BANS" | "GUILD_SETTINGS_LOADED_INTEGRATIONS" | "GUILD_SETTINGS_LOADED_INTEGRATIONS_WITH_COMMANDS" | "GUILD_SETTINGS_LOADED_INVITES" | "GUILD_SETTINGS_ONBOARDING_EDUCATION_UPSELL_DISMISSED" | "GUILD_SETTINGS_ONBOARDING_PROMPTS_EDIT" | "GUILD_SETTINGS_ONBOARDING_PROMPTS_ERRORS" | "GUILD_SETTINGS_ONBOARDING_PROMPTS_RESET" | "GUILD_SETTINGS_ONBOARDING_PROMPTS_SAVE_FAILED" | "GUILD_SETTINGS_ONBOARDING_PROMPTS_SAVE_SUCCESS" | "GUILD_SETTINGS_ONBOARDING_PROMPTS_SUBMIT" | "GUILD_SETTINGS_ONBOARDING_STEP" | "GUILD_SETTINGS_OPEN" | "GUILD_SETTINGS_ROLES_CLEAR_PERMISSIONS" | "GUILD_SETTINGS_ROLES_EDIT_SECTION_UPDATE" | "GUILD_SETTINGS_ROLES_INIT" | "GUILD_SETTINGS_ROLES_SAVE_FAIL" | "GUILD_SETTINGS_ROLES_SAVE_SUCCESS" | "GUILD_SETTINGS_ROLES_SORT_UPDATE" | "GUILD_SETTINGS_ROLES_SUBMITTING" | "GUILD_SETTINGS_ROLES_UPDATE_COLOR" | "GUILD_SETTINGS_ROLES_UPDATE_DESCRIPTION" | "GUILD_SETTINGS_ROLES_UPDATE_NAME" | "GUILD_SETTINGS_ROLES_UPDATE_PERMISSIONS" | "GUILD_SETTINGS_ROLES_UPDATE_PERMISSION_SET" | "GUILD_SETTINGS_ROLES_UPDATE_ROLE_CONNECTION_CONFIGURATIONS" | "GUILD_SETTINGS_ROLES_UPDATE_ROLE_ICON" | "GUILD_SETTINGS_ROLES_UPDATE_SETTINGS" | "GUILD_SETTINGS_ROLE_SELECT" | "GUILD_SETTINGS_SAVE_ROUTE_STACK" | "GUILD_SETTINGS_SET_MFA_SUCCESS" | "GUILD_SETTINGS_SET_SEARCH_QUERY" | "GUILD_SETTINGS_SET_SECTION" | "GUILD_SETTINGS_SET_VANITY_URL" | "GUILD_SETTINGS_SET_WIDGET" | "GUILD_SETTINGS_SUBMIT" | "GUILD_SETTINGS_SUBMIT_FAILURE" | "GUILD_SETTINGS_SUBMIT_SUCCESS" | "GUILD_SETTINGS_UPDATE" | "GUILD_SETTINGS_VANITY_URL_CLOSE" | "GUILD_SETTINGS_VANITY_URL_ERROR" | "GUILD_SETTINGS_VANITY_URL_RESET" | "GUILD_SETTINGS_VANITY_URL_SET" | "GUILD_SOUNDBOARD_DELETE_SUCCESS" | "GUILD_SOUNDBOARD_FETCH" | "GUILD_SOUNDBOARD_FETCH_FAILURE" | "GUILD_SOUNDBOARD_FETCH_SUCCESS" | "GUILD_SOUNDBOARD_SOUND_PLAY_END" | "GUILD_SOUNDBOARD_SOUND_PLAY_START" | "GUILD_SOUNDBOARD_UPDATE_SUCCESS" | "GUILD_SOUNDBOARD_UPLOAD_SUCCESS" | "GUILD_STICKERS_CREATE_SUCCESS" | "GUILD_STICKERS_FETCH_SUCCESS" | "GUILD_STICKERS_UPDATE" | "GUILD_STOP_LURKING" | "GUILD_STOP_LURKING_FAILURE" | "GUILD_SUBSCRIPTIONS" | "GUILD_SUBSCRIPTIONS_CHANNEL" | "GUILD_SUBSCRIPTIONS_FLUSH" | "GUILD_SUBSCRIPTIONS_MEMBERS_ADD" | "GUILD_SUBSCRIPTIONS_MEMBERS_REMOVE" | "GUILD_TEMPLATE_ACCEPT" | "GUILD_TEMPLATE_ACCEPT_FAILURE" | "GUILD_TEMPLATE_ACCEPT_SUCCESS" | "GUILD_TEMPLATE_CREATE_SUCCESS" | "GUILD_TEMPLATE_DELETE_SUCCESS" | "GUILD_TEMPLATE_DIRTY_TOOLTIP_HIDE" | "GUILD_TEMPLATE_DIRTY_TOOLTIP_REFRESH" | "GUILD_TEMPLATE_LOAD_FOR_GUILD_SUCCESS" | "GUILD_TEMPLATE_MODAL_HIDE" | "GUILD_TEMPLATE_MODAL_SHOW" | "GUILD_TEMPLATE_PROMOTION_TOOLTIP_HIDE" | "GUILD_TEMPLATE_RESOLVE" | "GUILD_TEMPLATE_RESOLVE_FAILURE" | "GUILD_TEMPLATE_RESOLVE_SUCCESS" | "GUILD_TEMPLATE_SYNC_SUCCESS" | "GUILD_TOGGLE_COLLAPSE_MUTED" | "GUILD_TOP_READ_CHANNELS_FETCH_SUCCESS" | "GUILD_UNAPPLY_BOOST_FAIL" | "GUILD_UNAPPLY_BOOST_START" | "GUILD_UNAPPLY_BOOST_SUCCESS" | "GUILD_UNAVAILABLE" | "GUILD_UNREADS_SET_LAST_CLEARED" | "GUILD_UPDATE" | "GUILD_UPDATE_DISCOVERY_METADATA" | "GUILD_UPDATE_DISCOVERY_METADATA_FAIL" | "GUILD_UPDATE_DISCOVERY_METADATA_FROM_SERVER" | "GUILD_VERIFICATION_CHECK" | "HIDE_ACTION_SHEET" | "HIDE_ACTION_SHEET_QUICK_SWITCHER" | "HIDE_KEYBOARD_SHORTCUTS" | "HIDE_SPAM_MESSAGES_FOR_USER" | "HOTSPOT_HIDE" | "HOTSPOT_OVERRIDE_CLEAR" | "HOTSPOT_OVERRIDE_SET" | "HYPESQUAD_ONLINE_MEMBERSHIP_JOIN_SUCCESS" | "HYPESQUAD_ONLINE_MEMBERSHIP_LEAVE_SUCCESS" | "I18N_LOAD_ERROR" | "I18N_LOAD_START" | "I18N_LOAD_SUCCESS" | "IDLE" | "IMPERSONATE_STOP" | "IMPERSONATE_UPDATE" | "INBOX_OPEN" | "INCOMING_CALL_MOVE" | "INSTALLATION_LOCATION_ADD" | "INSTALLATION_LOCATION_FETCH_METADATA" | "INSTALLATION_LOCATION_REMOVE" | "INSTALLATION_LOCATION_UPDATE" | "INSTANT_INVITE_CREATE" | "INSTANT_INVITE_CREATE_FAILURE" | "INSTANT_INVITE_CREATE_SUCCESS" | "INSTANT_INVITE_REVOKE_SUCCESS" | "INTEGRATION_PERMISSION_SETTINGS_APPLICATION_PERMISSIONS_FETCH_FAILURE" | "INTEGRATION_PERMISSION_SETTINGS_CLEAR" | "INTEGRATION_PERMISSION_SETTINGS_COMMANDS_FETCH_FAILURE" | "INTEGRATION_PERMISSION_SETTINGS_COMMANDS_FETCH_SUCCESS" | "INTEGRATION_PERMISSION_SETTINGS_COMMAND_UPDATE" | "INTEGRATION_PERMISSION_SETTINGS_EDIT" | "INTEGRATION_PERMISSION_SETTINGS_INIT" | "INTEGRATION_PERMISSION_SETTINGS_RESET" | "INTEGRATION_QUERY" | "INTEGRATION_QUERY_FAILURE" | "INTEGRATION_QUERY_SUCCESS" | "INTEGRATION_SETTINGS_INIT" | "INTEGRATION_SETTINGS_SAVE_FAILURE" | "INTEGRATION_SETTINGS_SAVE_SUCCESS" | "INTEGRATION_SETTINGS_SET_SECTION" | "INTEGRATION_SETTINGS_START_EDITING_COMMAND" | "INTEGRATION_SETTINGS_START_EDITING_INTEGRATION" | "INTEGRATION_SETTINGS_START_EDITING_WEBHOOK" | "INTEGRATION_SETTINGS_STOP_EDITING_COMMAND" | "INTEGRATION_SETTINGS_STOP_EDITING_INTEGRATION" | "INTEGRATION_SETTINGS_STOP_EDITING_WEBHOOK" | "INTEGRATION_SETTINGS_SUBMITTING" | "INTEGRATION_SETTINGS_UPDATE_INTEGRATION" | "INTEGRATION_SETTINGS_UPDATE_WEBHOOK" | "INTERACTION_CREATE" | "INTERACTION_FAILURE" | "INTERACTION_MODAL_CREATE" | "INTERACTION_QUEUE" | "INTERACTION_SUCCESS" | "INVITE_ACCEPT" | "INVITE_ACCEPT_FAILURE" | "INVITE_ACCEPT_SUCCESS" | "INVITE_APP_NOT_OPENED" | "INVITE_APP_OPENED" | "INVITE_APP_OPENING" | "INVITE_MODAL_CLOSE" | "INVITE_MODAL_ERROR" | "INVITE_MODAL_OPEN" | "INVITE_RESOLVE" | "INVITE_RESOLVE_FAILURE" | "INVITE_RESOLVE_SUCCESS" | "KEYBINDS_ADD_KEYBIND" | "KEYBINDS_DELETE_KEYBIND" | "KEYBINDS_ENABLE_ALL_KEYBINDS" | "KEYBINDS_REGISTER_GLOBAL_KEYBIND_ACTIONS" | "KEYBINDS_SET_KEYBIND" | "KEYBOARD_NAVIGATION_EXPLAINER_MODAL_SEEN" | "LAYER_POP" | "LAYER_POP_ALL" | "LAYER_PUSH" | "LAYOUT_CREATE" | "LAYOUT_CREATE_WIDGETS" | "LAYOUT_DELETE_ALL_WIDGETS" | "LAYOUT_DELETE_WIDGET" | "LAYOUT_SET_PINNED" | "LAYOUT_SET_TOP_WIDGET" | "LAYOUT_UPDATE_WIDGET" | "LIBRARY_APPLICATIONS_TEST_MODE_ENABLED" | "LIBRARY_APPLICATION_ACTIVE_BRANCH_UPDATE" | "LIBRARY_APPLICATION_ACTIVE_LAUNCH_OPTION_UPDATE" | "LIBRARY_APPLICATION_FILTER_UPDATE" | "LIBRARY_APPLICATION_FLAGS_UPDATE_START" | "LIBRARY_APPLICATION_FLAGS_UPDATE_SUCCESS" | "LIBRARY_APPLICATION_UPDATE" | "LIBRARY_FETCH_SUCCESS" | "LIBRARY_TABLE_ACTIVE_ROW_ID_UPDATE" | "LIBRARY_TABLE_SORT_UPDATE" | "LIVE_CHANNEL_NOTICE_HIDE" | "LOAD_ARCHIVED_THREADS" | "LOAD_ARCHIVED_THREADS_FAIL" | "LOAD_ARCHIVED_THREADS_SUCCESS" | "LOAD_FORUM_POSTS" | "LOAD_FRIEND_SUGGESTIONS_FAILURE" | "LOAD_FRIEND_SUGGESTIONS_SUCCESS" | "LOAD_GUILD_AFFINITIES_SUCCESS" | "LOAD_MESSAGES" | "LOAD_MESSAGES_AROUND_SUCCESS" | "LOAD_MESSAGES_FAILURE" | "LOAD_MESSAGES_SUCCESS" | "LOAD_MESSAGES_SUCCESS_CACHED" | "LOAD_MESSAGE_INTERACTION_DATA_SUCCESS" | "LOAD_MESSAGE_REQUESTS_SUPPLEMENTAL_DATA_ERROR" | "LOAD_MESSAGE_REQUESTS_SUPPLEMENTAL_DATA_SUCCESS" | "LOAD_NOTIFICATION_CENTER_ITEMS" | "LOAD_NOTIFICATION_CENTER_ITEMS_FAILURE" | "LOAD_NOTIFICATION_CENTER_ITEMS_SUCCESS" | "LOAD_PINNED_MESSAGES" | "LOAD_PINNED_MESSAGES_FAILURE" | "LOAD_PINNED_MESSAGES_SUCCESS" | "LOAD_RECENT_MENTIONS" | "LOAD_RECENT_MENTIONS_FAILURE" | "LOAD_RECENT_MENTIONS_SUCCESS" | "LOAD_REGIONS" | "LOAD_RELATIONSHIPS_FAILURE" | "LOAD_RELATIONSHIPS_SUCCESS" | "LOAD_THREADS_SUCCESS" | "LOAD_USER_AFFINITIES" | "LOAD_USER_AFFINITIES_FAILURE" | "LOAD_USER_AFFINITIES_SUCCESS" | "LOBBY_CONNECT" | "LOBBY_CREATE" | "LOBBY_DELETE" | "LOBBY_DISCONNECT" | "LOBBY_MEMBER_CONNECT" | "LOBBY_MEMBER_DISCONNECT" | "LOBBY_MEMBER_UPDATE" | "LOBBY_MESSAGE" | "LOBBY_UPDATE" | "LOBBY_VOICE_CONNECT" | "LOBBY_VOICE_DISCONNECT" | "LOBBY_VOICE_SERVER_UPDATE" | "LOBBY_VOICE_STATE_UPDATE" | "LOCAL_ACTIVITY_UPDATE" | "LOGIN" | "LOGIN_ACCOUNT_DISABLED" | "LOGIN_ACCOUNT_SCHEDULED_FOR_DELETION" | "LOGIN_ATTEMPTED" | "LOGIN_FAILURE" | "LOGIN_MFA" | "LOGIN_MFA_FAILURE" | "LOGIN_MFA_SMS" | "LOGIN_MFA_SMS_FAILURE" | "LOGIN_MFA_SMS_REQUEST_SUCCESS" | "LOGIN_MFA_STEP" | "LOGIN_MFA_WEBAUTHN" | "LOGIN_MFA_WEBAUTHN_TO_TOTP" | "LOGIN_PASSWORD_RECOVERY_PHONE_VERIFICATION" | "LOGIN_PHONE_IP_AUTHORIZATION_REQUIRED" | "LOGIN_RESET" | "LOGIN_STATUS_RESET" | "LOGIN_SUCCESS" | "LOGOUT" | "LOGOUT_AUTH_SESSIONS_SUCCESS" | "MASKED_LINK_ADD_TRUSTED_DOMAIN" | "MASKED_LINK_ADD_TRUSTED_PROTOCOL" | "MAX_MEMBER_COUNT_NOTICE_DISMISS" | "MEDIA_ENGINE_APPLY_MEDIA_FILTER_SETTINGS" | "MEDIA_ENGINE_APPLY_MEDIA_FILTER_SETTINGS_ERROR" | "MEDIA_ENGINE_APPLY_MEDIA_FILTER_SETTINGS_START" | "MEDIA_ENGINE_DEVICES" | "MEDIA_ENGINE_ENABLE_SOUNDSHARE" | "MEDIA_ENGINE_INTERACTION_REQUIRED" | "MEDIA_ENGINE_NOISE_CANCELLATION_ERROR_RESET" | "MEDIA_ENGINE_PERMISSION" | "MEDIA_ENGINE_SET_AEC_DUMP" | "MEDIA_ENGINE_SET_AUDIO_ENABLED" | "MEDIA_ENGINE_SET_AV1" | "MEDIA_ENGINE_SET_DESKTOP_SOURCE" | "MEDIA_ENGINE_SET_EXPERIMENTAL_ENCODERS" | "MEDIA_ENGINE_SET_EXPERIMENTAL_SOUNDSHARE" | "MEDIA_ENGINE_SET_HARDWARE_H264" | "MEDIA_ENGINE_SET_OPEN_H264" | "MEDIA_ENGINE_SET_VIDEO_DEVICE" | "MEDIA_ENGINE_SET_VIDEO_ENABLED" | "MEDIA_ENGINE_SET_VIDEO_HOOK" | "MEDIA_ENGINE_SOUNDSHARE_FAILED" | "MEDIA_ENGINE_SOUNDSHARE_TRANSMITTING" | "MEDIA_ENGINE_VIDEO_SOURCE_QUALITY_CHANGED" | "MEDIA_ENGINE_VIDEO_STATE_CHANGED" | "MEMBER_VERIFICATION_FORM_FETCH_FAIL" | "MEMBER_VERIFICATION_FORM_UPDATE" | "MENTION_MODAL_CLOSE" | "MENTION_MODAL_OPEN" | "MESSAGE_ACK" | "MESSAGE_CREATE" | "MESSAGE_DELETE" | "MESSAGE_DELETE_BULK" | "MESSAGE_EDIT_FAILED_AUTOMOD" | "MESSAGE_END_EDIT" | "MESSAGE_LENGTH_UPSELL" | "MESSAGE_REACTION_ADD" | "MESSAGE_REACTION_ADD_USERS" | "MESSAGE_REACTION_REMOVE" | "MESSAGE_REACTION_REMOVE_ALL" | "MESSAGE_REACTION_REMOVE_EMOJI" | "MESSAGE_REQUEST_ACCEPT_OPTIMISTIC" | "MESSAGE_REVEAL" | "MESSAGE_SEND_FAILED" | "MESSAGE_SEND_FAILED_AUTOMOD" | "MESSAGE_START_EDIT" | "MESSAGE_TODO_ADD" | "MESSAGE_TODO_CLEANUP" | "MESSAGE_TODO_COMPLETE" | "MESSAGE_UPDATE" | "MESSAGE_UPDATE_EDIT" | "MFA_CLEAR_BACKUP_CODES" | "MFA_DISABLE_SUCCESS" | "MFA_ENABLE_SUCCESS" | "MFA_SEND_VERIFICATION_KEY" | "MFA_SMS_TOGGLE" | "MFA_SMS_TOGGLE_COMPLETE" | "MFA_VIEW_BACKUP_CODES" | "MFA_WEBAUTHN_CREDENTIALS_LOADED" | "MFA_WEBAUTHN_CREDENTIALS_LOADING" | "MFA_WEBAUTHN_CREDENTIAL_CREATE" | "MFA_WEBAUTHN_CREDENTIAL_DELETE" | "MOBILE_WEB_SIDEBAR_CLOSE" | "MOBILE_WEB_SIDEBAR_OPEN" | "MODAL_POP" | "MODAL_PUSH" | "MULTI_ACCOUNT_INVALIDATE_PUSH_SYNC_TOKENS" | "MULTI_ACCOUNT_MOBILE_EXPERIMENT_UPDATE" | "MULTI_ACCOUNT_MOVE_ACCOUNT" | "MULTI_ACCOUNT_REMOVE_ACCOUNT" | "MULTI_ACCOUNT_UPDATE_PUSH_SYNC_TOKEN" | "MULTI_ACCOUNT_VALIDATE_TOKEN_FAILURE" | "MULTI_ACCOUNT_VALIDATE_TOKEN_REQUEST" | "MULTI_ACCOUNT_VALIDATE_TOKEN_SUCCESS" | "MUTUAL_FRIENDS_FETCH_FAILURE" | "MUTUAL_FRIENDS_FETCH_START" | "MUTUAL_FRIENDS_FETCH_SUCCESS" | "NEWLY_ADDED_EMOJI_SEEN_ACKNOWLEDGED" | "NEWLY_ADDED_EMOJI_SEEN_PENDING" | "NEWLY_ADDED_EMOJI_SEEN_UPDATED" | "NEW_PAYMENT_SOURCE_ADDRESS_INFO_UPDATE" | "NEW_PAYMENT_SOURCE_CARD_INFO_UPDATE" | "NEW_PAYMENT_SOURCE_CLEAR_ERROR" | "NEW_PAYMENT_SOURCE_STRIPE_PAYMENT_REQUEST_UPDATE" | "NITRODUCTION_PERSISTENT_ONBOARDING_TOGGLE_COLLAPSE" | "NOTICE_DISABLE" | "NOTICE_DISMISS" | "NOTICE_SHOW" | "NOTIFICATIONS_SET_DESKTOP_TYPE" | "NOTIFICATIONS_SET_DISABLED_SOUNDS" | "NOTIFICATIONS_SET_DISABLE_UNREAD_BADGE" | "NOTIFICATIONS_SET_NOTIFY_MESSAGES_IN_SELECTED_CHANNEL" | "NOTIFICATIONS_SET_PERMISSION_STATE" | "NOTIFICATIONS_SET_TASKBAR_FLASH" | "NOTIFICATIONS_SET_TTS_TYPE" | "NOTIFICATIONS_TOGGLE_ALL_DISABLED" | "NOTIFICATION_CENTER_ITEMS_ACK" | "NOTIFICATION_CENTER_ITEMS_ACK_FAILURE" | "NOTIFICATION_CENTER_ITEMS_LOCAL_ACK" | "NOTIFICATION_CENTER_ITEM_COMPLETED" | "NOTIFICATION_CENTER_ITEM_CREATE" | "NOTIFICATION_CENTER_ITEM_DELETE" | "NOTIFICATION_CENTER_ITEM_DELETE_FAILURE" | "NOTIFICATION_CENTER_SET_ACTIVE" | "NOTIFICATION_CENTER_SET_TAB" | "NOTIFICATION_CLICK" | "NOTIFICATION_CREATE" | "NOW_PLAYING_MOUNTED" | "NOW_PLAYING_UNMOUNTED" | "NUF_COMPLETE" | "NUF_NEW_USER" | "OAUTH2_TOKEN_REVOKE" | "ONLINE_GUILD_MEMBER_COUNT_UPDATE" | "OUTBOUND_PROMOTIONS_SEEN" | "OUTBOUND_PROMOTION_NOTICE_DISMISS" | "OVERLAY_ACTIVATE_REGION" | "OVERLAY_CALL_PRIVATE_CHANNEL" | "OVERLAY_CRASHED" | "OVERLAY_DEACTIVATE_ALL_REGIONS" | "OVERLAY_DISABLE_EXTERNAL_LINK_ALERT" | "OVERLAY_FOCUSED" | "OVERLAY_GAMES_CHANGE" | "OVERLAY_INCOMPATIBLE_APP" | "OVERLAY_INITIALIZE" | "OVERLAY_JOIN_GAME" | "OVERLAY_READY" | "OVERLAY_SELECT_CALL" | "OVERLAY_SELECT_CHANNEL" | "OVERLAY_SET_AVATAR_SIZE_MODE" | "OVERLAY_SET_DISPLAY_NAME_MODE" | "OVERLAY_SET_DISPLAY_USER_MODE" | "OVERLAY_SET_ENABLED" | "OVERLAY_SET_INPUT_LOCKED" | "OVERLAY_SET_NOTIFICATION_POSITION_MODE" | "OVERLAY_SET_NOT_IDLE" | "OVERLAY_SET_PREVIEW_IN_GAME_MODE" | "OVERLAY_SET_TEXT_CHAT_NOTIFICATION_MODE" | "OVERLAY_SET_TEXT_WIDGET_OPACITY" | "OVERLAY_SET_UI_LOCKED" | "OVERLAY_START_SESSION" | "PARTNER_REQUIREMENTS_FETCH_FAILURE" | "PARTNER_REQUIREMENTS_FETCH_START" | "PARTNER_REQUIREMENTS_FETCH_SUCCESS" | "PASSIVE_UPDATE_V1" | "PASSWORD_UPDATED" | "PAYMENT_AUTHENTICATION_CLEAR_ERROR" | "PAYMENT_AUTHENTICATION_ERROR" | "PAYMENT_UPDATE" | "PERMISSION_CLEAR_ELEVATED_PROCESS" | "PERMISSION_CLEAR_PTT_ADMIN_WARNING" | "PERMISSION_CLEAR_SUPPRESS_WARNING" | "PERMISSION_CLEAR_VAD_WARNING" | "PERMISSION_CONTINUE_NONELEVATED_PROCESS" | "PERMISSION_REQUEST_ELEVATED_PROCESS" | "PHONE_SET_COUNTRY_CODE" | "PICTURE_IN_PICTURE_CLOSE" | "PICTURE_IN_PICTURE_HIDE" | "PICTURE_IN_PICTURE_MOVE" | "PICTURE_IN_PICTURE_OPEN" | "PICTURE_IN_PICTURE_SHOW" | "PICTURE_IN_PICTURE_UPDATE_RECT" | "PICTURE_IN_PICTURE_UPDATE_SELECTED_WINDOW" | "POGGERMODE_ACHIEVEMENT_UNLOCK" | "POGGERMODE_SETTINGS_UPDATE" | "POGGERMODE_TEMPORARILY_DISABLED" | "POGGERMODE_UPDATE_COMBO" | "POGGERMODE_UPDATE_MESSAGE_COMBO" | "POPOUT_WINDOW_CLOSE" | "POPOUT_WINDOW_OPEN" | "POPOUT_WINDOW_SET_ALWAYS_ON_TOP" | "POST_CONNECTION_OPEN" | "PREMIUM_PAYMENT_ERROR_CLEAR" | "PREMIUM_PAYMENT_MODAL_CLOSE" | "PREMIUM_PAYMENT_MODAL_OPEN" | "PREMIUM_PAYMENT_SUBSCRIBE_FAIL" | "PREMIUM_PAYMENT_SUBSCRIBE_START" | "PREMIUM_PAYMENT_SUBSCRIBE_SUCCESS" | "PREMIUM_PAYMENT_UPDATE_FAIL" | "PREMIUM_PAYMENT_UPDATE_SUCCESS" | "PREMIUM_REQUIRED_MODAL_CLOSE" | "PREMIUM_REQUIRED_MODAL_OPEN" | "PRESENCES_REPLACE" | "PRESENCE_UPDATES" | "PRIVATE_CHANNEL_RECIPIENTS_ADD_USER" | "PRIVATE_CHANNEL_RECIPIENTS_INVITE_CLOSE" | "PRIVATE_CHANNEL_RECIPIENTS_INVITE_OPEN" | "PRIVATE_CHANNEL_RECIPIENTS_INVITE_QUERY" | "PRIVATE_CHANNEL_RECIPIENTS_INVITE_SELECT" | "PRIVATE_CHANNEL_RECIPIENTS_REMOVE_USER" | "PROFILE_CUSTOMIZATION_OPEN_PREVIEW_MODAL" | "PROFILE_PANEL_TOGGLE_SECTION" | "PUBLIC_UPSELL_NOTICE_DISMISS" | "PURCHASE_CONFIRMATION_MODAL_CLOSE" | "PURCHASE_CONFIRMATION_MODAL_OPEN" | "PUSH_NOTIFICATION_CLICK" | "QUEUE_INTERACTION_COMPONENT_STATE" | "QUICKSWITCHER_HIDE" | "QUICKSWITCHER_SEARCH" | "QUICKSWITCHER_SELECT" | "QUICKSWITCHER_SHOW" | "QUICKSWITCHER_SWITCH_TO" | "RECENT_MENTION_DELETE" | "REGISTER" | "REGISTER_FAILURE" | "REGISTER_SAVE_FORM" | "REGISTER_SUCCESS" | "RELATIONSHIP_ADD" | "RELATIONSHIP_REMOVE" | "RELATIONSHIP_UPDATE" | "REMOTE_COMMAND" | "REMOTE_SESSION_CONNECT" | "REMOTE_SESSION_DISCONNECT" | "REMOVE_AUTOMOD_MESSAGE_NOTICE" | "REQUEST_FORUM_UNREADS" | "RESET_ALL_NITRODUCTION_TOOLTIPS" | "RESET_HAS_COMPLETED_STEP" | "RESET_NOTIFICATION_CENTER" | "RESET_PAYMENT_ID" | "RESORT_THREADS" | "RPC_APP_AUTHENTICATED" | "RPC_APP_CONNECTED" | "RPC_APP_DISCONNECTED" | "RPC_NOTIFICATION_CREATE" | "RPC_SERVER_READY" | "RTC_CONNECTION_LOSS_RATE" | "RTC_CONNECTION_PING" | "RTC_CONNECTION_STATE" | "RTC_CONNECTION_UPDATE_ID" | "RTC_CONNECTION_VIDEO" | "RTC_DEBUG_MODAL_CLOSE" | "RTC_DEBUG_MODAL_OPEN" | "RTC_DEBUG_MODAL_OPEN_REPLAY" | "RTC_DEBUG_MODAL_OPEN_REPLAY_AT_PATH" | "RTC_DEBUG_MODAL_SET_SECTION" | "RTC_DEBUG_MODAL_UPDATE" | "RTC_DEBUG_MODAL_UPDATE_VIDEO_OUTPUT" | "RTC_DEBUG_POPOUT_WINDOW_OPEN" | "RTC_DEBUG_SET_RECORDING_FLAG" | "RTC_LATENCY_TEST_COMPLETE" | "RUNNING_GAMES_CHANGE" | "RUNNING_GAME_ADD_OVERRIDE" | "RUNNING_GAME_DELETE_ENTRY" | "RUNNING_GAME_EDIT_NAME" | "RUNNING_GAME_TOGGLE_OVERLAY" | "RUNNING_STREAMER_TOOLS_CHANGE" | "SAVE_LAST_NON_VOICE_ROUTE" | "SAVE_LAST_ROUTE" | "SEARCH_AUTOCOMPLETE_QUERY_UPDATE" | "SEARCH_CLEAR_HISTORY" | "SEARCH_EDITOR_STATE_CHANGE" | "SEARCH_EDITOR_STATE_CLEAR" | "SEARCH_ENSURE_SEARCH_STATE" | "SEARCH_FINISH" | "SEARCH_INDEXING" | "SEARCH_MODAL_CLOSE" | "SEARCH_MODAL_OPEN" | "SEARCH_SET_SHOW_BLOCKED_RESULTS" | "SEARCH_START" | "SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE" | "SELF_PRESENCE_STORE_UPDATE" | "SESSIONS_REPLACE" | "SET_CHANNEL_BITRATE" | "SET_CHANNEL_VIDEO_QUALITY_MODE" | "SET_CONSENT_REQUIRED" | "SET_GUILD_FOLDER_EXPANDED" | "SET_HAS_COMPLETED_STEP" | "SET_INTERACTION_COMPONENT_STATE" | "SET_LOCATION_METADATA" | "SET_LOGIN_CREDENTIALS" | "SET_NATIVE_PERMISSION" | "SET_PENDING_REPLY_SHOULD_MENTION" | "SET_RECENT_MENTIONS_FILTER" | "SET_RECENT_MENTIONS_STALE" | "SET_SOUNDPACK" | "SET_TTS_SPEECH_RATE" | "SET_VAD_PERMISSION" | "SHOW_ACTION_SHEET" | "SHOW_ACTION_SHEET_QUICK_SWITCHER" | "SHOW_KEYBOARD_SHORTCUTS" | "SIDEBAR_CLOSE" | "SIDEBAR_CREATE_THREAD" | "SIDEBAR_VIEW_CHANNEL" | "SKUS_FETCH_SUCCESS" | "SKU_FETCH_FAIL" | "SKU_FETCH_START" | "SKU_FETCH_SUCCESS" | "SKU_PURCHASE_CLEAR_ERROR" | "SKU_PURCHASE_FAIL" | "SKU_PURCHASE_MODAL_CLOSE" | "SKU_PURCHASE_MODAL_OPEN" | "SKU_PURCHASE_PREVIEW_FETCH_SUCCESS" | "SKU_PURCHASE_SHOW_CONFIRMATION_STEP" | "SKU_PURCHASE_START" | "SKU_PURCHASE_SUCCESS" | "SKU_PURCHASE_UPDATE_IS_GIFT" | "SLOWMODE_RESET_COOLDOWN" | "SLOWMODE_SET_COOLDOWN" | "SOUNDBOARD_ADD_FAVORITE_SOUND" | "SOUNDBOARD_REMOVE_FAVORITE_SOUND" | "SPEAKING" | "SPEAKING_MESSAGE" | "SPEAK_MESSAGE" | "SPEAK_TEXT" | "SPELLCHECK_LEARN_WORD" | "SPELLCHECK_TOGGLE" | "SPOTIFY_ACCOUNT_ACCESS_TOKEN" | "SPOTIFY_ACCOUNT_ACCESS_TOKEN_REVOKE" | "SPOTIFY_PLAYER_PAUSE" | "SPOTIFY_PLAYER_PLAY" | "SPOTIFY_PLAYER_STATE" | "SPOTIFY_PROFILE_UPDATE" | "SPOTIFY_SET_ACTIVE_DEVICE" | "SPOTIFY_SET_DEVICES" | "SPOTIFY_SET_PROTOCOL_REGISTERED" | "STAGE_INSTANCE_CREATE" | "STAGE_INSTANCE_DELETE" | "STAGE_INSTANCE_UPDATE" | "START_SESSION" | "STATUS_PAGE_INCIDENT" | "STATUS_PAGE_SCHEDULED_MAINTENANCE" | "STATUS_PAGE_SCHEDULED_MAINTENANCE_ACK" | "STICKER_FETCH_SUCCESS" | "STICKER_PACKS_FETCH_START" | "STICKER_PACKS_FETCH_SUCCESS" | "STICKER_PACK_FETCH_SUCCESS" | "STICKER_TRACK_USAGE" | "STOP_SPEAKING" | "STORE_APPLICATION_INTERACTION_FAKE_USER" | "STORE_LISTINGS_FETCH_SUCCESS" | "STORE_LISTING_FETCH_SUCCESS" | "STREAMER_MODE_UPDATE" | "STREAMING_UPDATE" | "STREAM_CLOSE" | "STREAM_CREATE" | "STREAM_DELETE" | "STREAM_LAYOUT_UPDATE" | "STREAM_PREVIEW_FETCH_FAIL" | "STREAM_PREVIEW_FETCH_START" | "STREAM_PREVIEW_FETCH_SUCCESS" | "STREAM_SERVER_UPDATE" | "STREAM_SET_PAUSED" | "STREAM_START" | "STREAM_STATS_UPDATE" | "STREAM_STOP" | "STREAM_TIMED_OUT" | "STREAM_UPDATE" | "STREAM_UPDATE_SELF_HIDDEN" | "STREAM_UPDATE_SETTINGS" | "STREAM_WATCH" | "STRIPE_TOKEN_FAILURE" | "SUBSCRIPTION_PLANS_FETCH" | "SUBSCRIPTION_PLANS_FETCH_FAILURE" | "SUBSCRIPTION_PLANS_FETCH_SUCCESS" | "SUBSCRIPTION_PLANS_RESET" | "SURVEY_FETCHED" | "SURVEY_HIDE" | "SURVEY_OVERRIDE" | "SYSTEM_THEME_CHANGE" | "THERMAL_STATE_CHANGE" | "THREAD_CREATE" | "THREAD_CREATE_LOCAL" | "THREAD_DELETE" | "THREAD_LIST_SYNC" | "THREAD_MEMBERS_UPDATE" | "THREAD_MEMBER_LIST_UPDATE" | "THREAD_MEMBER_LOCAL_UPDATE" | "THREAD_MEMBER_UPDATE" | "THREAD_SETTINGS_DRAFT_CHANGE" | "THREAD_UPDATE" | "TOGGLE_GUILD_FOLDER_EXPAND" | "TOP_EMOJIS_FETCH" | "TOP_EMOJIS_FETCH_SUCCESS" | "TRUNCATE_MENTIONS" | "TRUNCATE_MESSAGES" | "TUTORIAL_INDICATOR_DISMISS" | "TUTORIAL_INDICATOR_HIDE" | "TUTORIAL_INDICATOR_SHOW" | "TUTORIAL_INDICATOR_SUPPRESS_ALL" | "TYPING_START" | "TYPING_START_LOCAL" | "TYPING_STOP" | "TYPING_STOP_LOCAL" | "UNSYNCED_USER_SETTINGS_UPDATE" | "UNVERIFIED_GAME_UPDATE" | "UPDATE_APP_COLORS" | "UPDATE_AVAILABLE" | "UPDATE_CHANNEL_DIMENSIONS" | "UPDATE_CHANNEL_LIST_DIMENSIONS" | "UPDATE_CONSENTS" | "UPDATE_DOWNLOADED" | "UPDATE_ERROR" | "UPDATE_GUILD_LIST_DIMENSIONS" | "UPDATE_HAS_FLOW_START_EVENT_BEEN_EMITTED" | "UPDATE_MANUALLY" | "UPDATE_NOT_AVAILABLE" | "UPDATE_TOKEN" | "UPLOAD_ATTACHMENT_ADD_FILES" | "UPLOAD_ATTACHMENT_CLEAR_ALL_FILES" | "UPLOAD_ATTACHMENT_POP_FILE" | "UPLOAD_ATTACHMENT_REMOVE_FILE" | "UPLOAD_ATTACHMENT_REMOVE_FILES" | "UPLOAD_ATTACHMENT_SET_FILE" | "UPLOAD_ATTACHMENT_SET_UPLOADS" | "UPLOAD_ATTACHMENT_UPDATE_FILE" | "UPLOAD_CANCEL_REQUEST" | "UPLOAD_COMPLETE" | "UPLOAD_COMPRESSION_PROGRESS" | "UPLOAD_FAIL" | "UPLOAD_PROGRESS" | "UPLOAD_RESTORE_FAILED_UPLOAD" | "UPLOAD_START" | "USER_ACHIEVEMENT_UPDATE" | "USER_ACTIVITY_STATISTICS_FETCH_SUCCESS" | "USER_APPLIED_BOOSTS_FETCH_SUCCESS" | "USER_AUTHORIZED_APPS_UPDATE" | "USER_CONNECTIONS_INTEGRATION_JOINING" | "USER_CONNECTIONS_UPDATE" | "USER_CONNECTION_UPDATE" | "USER_GUILD_JOIN_REQUEST_UPDATE" | "USER_GUILD_SETTINGS_CHANNEL_UPDATE" | "USER_GUILD_SETTINGS_CHANNEL_UPDATE_BULK" | "USER_GUILD_SETTINGS_FULL_UPDATE" | "USER_GUILD_SETTINGS_GUILD_AND_CHANNELS_UPDATE" | "USER_GUILD_SETTINGS_GUILD_UPDATE" | "USER_GUILD_SETTINGS_REMOVE_PENDING_CHANNEL_UPDATES" | "USER_JOIN_REQUEST_GUILDS_FETCH" | "USER_NON_CHANNEL_ACK" | "USER_NOTE_LOADED" | "USER_NOTE_LOAD_START" | "USER_NOTE_UPDATE" | "USER_PAYMENT_CLIENT_ADD" | "USER_PROFILE_ACCESSIBILITY_TOOLTIP_VIEWED" | "USER_PROFILE_FETCH_FAILURE" | "USER_PROFILE_FETCH_START" | "USER_PROFILE_FETCH_SUCCESS" | "USER_PROFILE_MODAL_CLOSE" | "USER_PROFILE_MODAL_OPEN" | "USER_PROFILE_UPDATE_SUCCESS" | "USER_REQUIRED_ACTION_UPDATE" | "USER_SETTINGS_ACCOUNT_CLOSE" | "USER_SETTINGS_ACCOUNT_INIT" | "USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM" | "USER_SETTINGS_ACCOUNT_SET_DISABLE_SUBMIT" | "USER_SETTINGS_ACCOUNT_SET_PENDING_ACCENT_COLOR" | "USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR" | "USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR_DECORATION" | "USER_SETTINGS_ACCOUNT_SET_PENDING_BANNER" | "USER_SETTINGS_ACCOUNT_SET_PENDING_BIO" | "USER_SETTINGS_ACCOUNT_SET_PENDING_PRONOUNS" | "USER_SETTINGS_ACCOUNT_SET_PENDING_THEME_COLORS" | "USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_AVATAR" | "USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_BANNER" | "USER_SETTINGS_ACCOUNT_SET_TRY_IT_OUT_THEME_COLORS" | "USER_SETTINGS_ACCOUNT_SUBMIT" | "USER_SETTINGS_ACCOUNT_SUBMIT_FAILURE" | "USER_SETTINGS_ACCOUNT_SUBMIT_SUCCESS" | "USER_SETTINGS_CLEAR_ERRORS" | "USER_SETTINGS_LOCALE_OVERRIDE" | "USER_SETTINGS_MODAL_CLEAR_SCROLL_POSITION" | "USER_SETTINGS_MODAL_CLEAR_SUBSECTION" | "USER_SETTINGS_MODAL_CLOSE" | "USER_SETTINGS_MODAL_INIT" | "USER_SETTINGS_MODAL_OPEN" | "USER_SETTINGS_MODAL_RESET" | "USER_SETTINGS_MODAL_SET_SECTION" | "USER_SETTINGS_MODAL_SUBMIT" | "USER_SETTINGS_MODAL_SUBMIT_COMPLETE" | "USER_SETTINGS_MODAL_SUBMIT_FAILURE" | "USER_SETTINGS_MODAL_UPDATE_ACCOUNT" | "USER_SETTINGS_OVERRIDE_APPLY" | "USER_SETTINGS_OVERRIDE_CLEAR" | "USER_SETTINGS_PROTO_ENQUEUE_UPDATE" | "USER_SETTINGS_PROTO_LOAD_IF_NECESSARY" | "USER_SETTINGS_PROTO_UPDATE" | "USER_SETTINGS_PROTO_UPDATE_EDIT_INFO" | "USER_SETTINGS_RESET_ALL_PENDING" | "USER_SETTINGS_RESET_ALL_TRY_IT_OUT" | "USER_SETTINGS_RESET_PENDING_ACCOUNT_CHANGES" | "USER_SETTINGS_RESET_PENDING_PROFILE_CHANGES" | "USER_SETTINGS_THEME_OVERRIDE" | "USER_UPDATE" | "VERIFY_FAILURE" | "VERIFY_SUCCESS" | "VIDEO_FILTER_ASSETS_FETCH_SUCCESS" | "VIDEO_FILTER_ASSET_DELETE_SUCCESS" | "VIDEO_FILTER_ASSET_UPLOAD_SUCCESS" | "VIDEO_SAVE_LAST_USED_BACKGROUND_OPTION" | "VIEW_HISTORY_MARK_VIEW" | "VOICE_CATEGORY_COLLAPSE" | "VOICE_CATEGORY_EXPAND" | "VOICE_CHANNEL_EFFECT_CLEAR" | "VOICE_CHANNEL_EFFECT_RECENT_EMOJI" | "VOICE_CHANNEL_EFFECT_SEND" | "VOICE_CHANNEL_EFFECT_SENT_LOCAL" | "VOICE_CHANNEL_EFFECT_TOGGLE_ANIMATION_TYPE" | "VOICE_CHANNEL_EFFECT_UPDATE_TIME_STAMP" | "VOICE_CHANNEL_SELECT" | "VOICE_SERVER_UPDATE" | "VOICE_STATE_UPDATES" | "WAIT_FOR_REMOTE_SESSION" | "WEBHOOKS_FETCHING" | "WEBHOOKS_UPDATE" | "WEBHOOK_CREATE" | "WEBHOOK_DELETE" | "WEBHOOK_UPDATE" | "WELCOME_SCREEN_FETCH_FAIL" | "WELCOME_SCREEN_FETCH_START" | "WELCOME_SCREEN_FETCH_SUCCESS" | "WELCOME_SCREEN_SETTINGS_CLEAR" | "WELCOME_SCREEN_SETTINGS_RESET" | "WELCOME_SCREEN_SETTINGS_UPDATE" | "WELCOME_SCREEN_SUBMIT" | "WELCOME_SCREEN_SUBMIT_FAILURE" | "WELCOME_SCREEN_SUBMIT_SUCCESS" | "WELCOME_SCREEN_UPDATE" | "WELCOME_SCREEN_VIEW" | "WINDOW_FOCUS" | "WINDOW_FULLSCREEN_CHANGE" | "WINDOW_HIDDEN" | "WINDOW_INIT" | "WINDOW_RESIZED" | "WINDOW_UNLOAD"; diff --git a/src/ipcMain/legacy.ts b/src/webpack/common/types/index.d.ts similarity index 58% rename from src/ipcMain/legacy.ts rename to src/webpack/common/types/index.d.ts index 567ad3d06..af4b5e1fb 100644 --- a/src/ipcMain/legacy.ts +++ b/src/webpack/common/types/index.d.ts @@ -1,6 +1,6 @@ /* * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors + * Copyright (c) 2023 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,16 +16,9 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import IpcEvents from "@utils/IpcEvents"; -import { ipcMain } from "electron"; -import { writeFile } from "fs/promises"; -import { join } from "path"; - -import { get } from "./simpleGet"; - -ipcMain.handleOnce(IpcEvents.DOWNLOAD_VENCORD_CSS, async () => { - const buf = await get("https://github.com/Vendicated/Vencord/releases/download/devbuild/renderer.css"); - await writeFile(join(__dirname, "renderer.css"), buf); - return buf.toString("utf-8"); -}); +export * from "./components"; +export * from "./fluxEvents"; +export * from "./menu"; +export * from "./stores"; +export * from "./utils"; diff --git a/src/webpack/common/types/menu.d.ts b/src/webpack/common/types/menu.d.ts new file mode 100644 index 000000000..e48d59458 --- /dev/null +++ b/src/webpack/common/types/menu.d.ts @@ -0,0 +1,68 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import type { ComponentType, CSSProperties, PropsWithChildren, UIEvent } from "react"; + +type RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>; + +export interface Menu { + ContextMenu: RC<{ + navId: string; + onClose(): void; + className?: string; + style?: CSSProperties; + hideScroller?: boolean; + onSelect?(): void; + }>; + MenuSeparator: ComponentType; + MenuGroup: RC<any>; + MenuItem: RC<{ + id: string; + label: string; + render?: ComponentType; + onChildrenScroll?: Function; + childRowHeight?: number; + listClassName?: string; + }>; + MenuCheckboxItem: RC<{ + id: string; + }>; + MenuRadioItem: RC<{ + id: string; + }>; + MenuControlItem: RC<{ + id: string; + interactive?: boolean; + }>; +} + +export interface ContextMenuApi { + close(): void; + open( + event: UIEvent, + render?: Menu["ContextMenu"], + options?: { enableSpellCheck?: boolean; }, + renderLazy?: () => Promise<Menu["ContextMenu"]> + ): void; + openLazy( + event: UIEvent, + renderLazy?: () => Promise<Menu["ContextMenu"]>, + options?: { enableSpellCheck?: boolean; } + ): void; +} + diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts new file mode 100644 index 000000000..6af5b27cd --- /dev/null +++ b/src/webpack/common/types/stores.d.ts @@ -0,0 +1,40 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { FluxDispatcher, FluxEvents } from "./utils"; + +export class FluxStore { + constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial<Record<FluxEvents, (data: any) => void>>); + + emitChange(): void; + getDispatchToken(): string; + getName(): string; + initialize(): void; + initializeIfNeeded(): void; + __getLocalVars(): Record<string, any>; +} + +export interface Flux { + Store: typeof FluxStore; +} + +export class WindowStore extends FluxStore { + isElementFullScreen(): boolean; + isFocused(): boolean; + windowSize(): Record<"width" | "height", number>; +} diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts new file mode 100644 index 000000000..0e2a6ca84 --- /dev/null +++ b/src/webpack/common/types/utils.d.ts @@ -0,0 +1,84 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import type { ReactNode } from "react"; + +import type { FluxEvents } from "./fluxEvents"; + +export { FluxEvents }; + +export interface FluxDispatcher { + _actionHandlers: any; + _subscriptions: any; + dispatch(event: { [key: string]: unknown; type: FluxEvents; }): Promise<void>; + isDispatching(): boolean; + subscribe(event: FluxEvents, callback: (data: any) => void): void; + unsubscribe(event: FluxEvents, callback: (data: any) => void): void; +} + +export type Parser = Record< + | "parse" + | "parseTopic" + | "parseEmbedTitle" + | "parseInlineReply" + | "parseGuildVerificationFormRule" + | "parseGuildEventDescription" + | "parseAutoModerationSystemMessage" + | "parseForumPostGuidelines" + | "parseForumPostMostRecentMessage", + (content: string, inline?: boolean, state?: Record<string, any>) => ReactNode[] +> & Record<"defaultRules" | "guildEventRules", Record<string, Record<"react" | "html" | "parse" | "match" | "order", any>>>; + +export interface Alerts { + show(alert: { + title: any; + body: React.ReactNode; + className?: string; + confirmColor?: string; + cancelText?: string; + confirmText?: string; + secondaryConfirmText?: string; + onCancel?(): void; + onConfirm?(): void; + onConfirmSecondary?(): void; + }): void; + /** This is a noop, it does nothing. */ + close(): void; +} + +export interface SnowflakeUtils { + fromTimestamp(timestamp: number): string; + extractTimestamp(snowflake: string): number; + age(snowflake: string): number; + atPreviousMillisecond(snowflake: string): string; + compare(snowflake1: string, snowflake2: string): number; +} + +interface RestRequestData { + url: string; + query?: Record<string, any>; + body?: Record<string, any>; + oldFormErrors?: boolean; + retries?: number; +} + +export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>> & { + V6OrEarlierAPIError: Error; + V8APIError: Error; + getAPIBaseURL(withVersion?: boolean): string; +}; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts new file mode 100644 index 000000000..b53c34082 --- /dev/null +++ b/src/webpack/common/utils.ts @@ -0,0 +1,111 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import type { User } from "discord-types/general"; + +// eslint-disable-next-line path-alias/no-relative +import { _resolveReady,filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy, waitFor } from "../webpack"; +import type * as t from "./types/utils"; + +export let FluxDispatcher: t.FluxDispatcher; + +export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get"); +export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); + +export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight"); + +export let SnowflakeUtils: t.SnowflakeUtils; +waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); + +export let Parser: t.Parser; +export let Alerts: t.Alerts; + +const ToastType = { + MESSAGE: 0, + SUCCESS: 1, + FAILURE: 2, + CUSTOM: 3 +}; +const ToastPosition = { + TOP: 0, + BOTTOM: 1 +}; + +export const Toasts = { + Type: ToastType, + Position: ToastPosition, + // what's less likely than getting 0 from Math.random()? Getting it twice in a row + genId: () => (Math.random() || Math.random()).toString(36).slice(2), + + // hack to merge with the following interface, dunno if there's a better way + ...{} as { + show(data: { + message: string, + id: string, + /** + * Toasts.Type + */ + type: number, + options?: { + /** + * Toasts.Position + */ + position?: number; + component?: React.ReactNode, + duration?: number; + }; + }): void; + pop(): void; + } +}; + +export const UserUtils = { + fetchUser: findByCodeLazy(".USER(", "getUser") as (id: string) => Promise<User>, +}; + +export const Clipboard = mapMangledModuleLazy('document.queryCommandEnabled("copy")||document.queryCommandSupported("copy")', { + copy: filters.byCode(".default.copy("), + SUPPORTS_COPY: x => typeof x === "boolean", +}); + +export const NavigationRouter = mapMangledModuleLazy("transitionToGuild - ", { + transitionTo: filters.byCode("transitionTo -"), + transitionToGuild: filters.byCode("transitionToGuild -"), + goBack: filters.byCode("goBack()"), + goForward: filters.byCode("goForward()"), +}); + +waitFor(["dispatch", "subscribe"], m => { + FluxDispatcher = m; + const cb = () => { + m.unsubscribe("CONNECTION_OPEN", cb); + _resolveReady(); + }; + m.subscribe("CONNECTION_OPEN", cb); +}); + + +// This is the same module but this is easier +waitFor(filters.byCode("currentToast?"), m => Toasts.show = m); +waitFor(filters.byCode("currentToast:null"), m => Toasts.pop = m); + +waitFor(["show", "close"], m => Alerts = m); +waitFor("parseTopic", m => Parser = m); + +export let SettingsRouter: any; +waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 7b318b218..19ca9517b 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -21,6 +21,7 @@ import Logger from "@utils/Logger"; import { canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; +import { traceFunction } from "../debug/Tracer"; import { _initWebpack } from "."; let webpackChunk: any[]; @@ -132,6 +133,7 @@ function patchPush() { for (let i = 0; i < patches.length; i++) { const patch = patches[i]; + const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); if (patch.predicate && !patch.predicate()) continue; if (code.includes(patch.find)) { @@ -146,7 +148,7 @@ function patchPush() { canonicalizeReplacement(replacement, patch.plugin); try { - const newCode = code.replace(replacement.match, replacement.replace as string); + const newCode = executePatch(replacement.match, replacement.replace as string); if (newCode === code && !patch.noWarn) { logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); if (IS_DEV) { @@ -187,7 +189,7 @@ function patchPush() { } logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); - logger.errorCustomFmt(...Logger.makeTitle("white", "After"), context); + logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 0bbd81506..5aa7dc724 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -50,6 +50,8 @@ export const filters = { } return true; }, + byStoreName: (name: string): FilterFn => m => + m.constructor?.displayName === name }; export const subscriptions = new Map<FilterFn, CallbackFn>(); @@ -326,6 +328,20 @@ export function findByCodeLazy(...code: string[]) { return findLazy(filters.byCode(...code)); } +/** + * Find a store by its displayName + */ +export function findStore(name: string) { + return find(filters.byStoreName(name)); +} + +/** + * findByDisplayName but lazy + */ +export function findStoreLazy(name: string) { + return findLazy(filters.byStoreName(name)); +} + /** * Wait for a module that matches the provided filter to be registered, * then call the callback with the module as the first argument diff --git a/test/generateReport.ts b/test/generateReport.ts index 9483225f4..d55cc8abe 100644 --- a/test/generateReport.ts +++ b/test/generateReport.ts @@ -186,8 +186,11 @@ page.on("console", async e => { } else if (isDebug) { console.error(e.text()); } else if (level === "error") { - console.error("Got unexpected error", e.text()); - report.otherErrors.push(e.text()); + const text = e.text(); + if (!text.startsWith("Failed to load resource: the server responded with a status of")) { + console.error("Got unexpected error", text); + report.otherErrors.push(text); + } } }); @@ -209,6 +212,7 @@ function runTime(token: string) { // Monkey patch Logger to not log with custom css + // @ts-ignore Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { if (level === "warn" || level === "error") console[level]("[Vencord]", this.name + ":", ...args); @@ -217,6 +221,9 @@ function runTime(token: string) { // force enable all plugins and patches Vencord.Plugins.patches.length = 0; Object.values(Vencord.Plugins.plugins).forEach(p => { + // Needs native server to run + if (p.name === "WebRichPresence (arRPC)") return; + p.required = true; p.patches?.forEach(patch => { patch.plugin = p.name; @@ -250,6 +257,8 @@ function runTime(token: string) { if (!isWasm) await wreq.e(id as any); + + await new Promise(r => setTimeout(r, 100)); } console.error("[PUP_DEBUG]", "Finished loading chunks!"); diff --git a/tsconfig.json b/tsconfig.json index a55c1fe36..db5407455 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "esModuleInterop": true, "lib": [ "DOM", + "DOM.Iterable", "esnext", "esnext.array", "esnext.asynciterable", @@ -18,11 +19,12 @@ "baseUrl": "./src/", "paths": { - "@webpack": ["./webpack"], - "@webpack/common": ["./webpack/common"], - "@utils/*": ["./utils/*"], "@api/*": ["./api/*"], - "@components/*": ["./components/*"] + "@components/*": ["./components/*"], + "@utils/*": ["./utils/*"], + "@webpack/types": ["./webpack/common/types"], + "@webpack/common": ["./webpack/common"], + "@webpack": ["./webpack/webpack"] } }, "include": ["src/**/*"]