Compare commits
4 Commits
v1.1.4
...
feat/relat
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e06b8d34c | ||
![]() |
b972aa1663 | ||
![]() |
3bf81ee0fa | ||
![]() |
486230a335 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -37,9 +37,6 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
||||||
- name: Generate plugin list
|
|
||||||
run: pnpm generatePluginJson dist/plugins.json
|
|
||||||
|
|
||||||
- name: Clean up obsolete files
|
- name: Clean up obsolete files
|
||||||
run: |
|
run: |
|
||||||
rm -rf dist/extension* Vencord.user.css
|
rm -rf dist/extension* Vencord.user.css
|
||||||
|
2
.github/workflows/reportBrokenPlugins.yml
vendored
2
.github/workflows/reportBrokenPlugins.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
esbuild test/generateReport.ts > dist/report.mjs
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
env:
|
env:
|
||||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
# Code of Conduct
|
|
||||||
|
|
||||||
Our community is welcoming to everyone, regardless of their characteristics.
|
|
||||||
|
|
||||||
As such, we expect you to treat everyone with respect and contribute to an open and welcoming community.
|
|
||||||
|
|
||||||
DO
|
|
||||||
- have empathy and be nice to others
|
|
||||||
- be respectful of differing opinions, even if you disagree
|
|
||||||
- give and accept constructive criticism
|
|
||||||
|
|
||||||
DON'T
|
|
||||||
- use offensive or derogatory language
|
|
||||||
- troll or spam
|
|
||||||
- personally attack or harass others
|
|
||||||
|
|
||||||
Repetitive violations of these guidelines might get your access to the repository restricted.
|
|
||||||
|
|
||||||
|
|
||||||
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from publicly challenging them and instead contact a Moderator on our Discord server or send an email to vendicated+conduct@riseup.net!
|
|
15
README.md
15
README.md
@ -4,14 +4,12 @@ The cutest Discord client mod
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Super easy to install (Download Installer, open, click install button, done)
|
- Super easy to install (one click installer)
|
||||||
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
- 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
|
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||||
- Fairly lightweight despite the many inbuilt plugins
|
|
||||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||||
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
|
||||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||||
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
|
- Works in all Electron versions (Confirmed working on versions 13-23)
|
||||||
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||||
|
|
||||||
## Installing / Uninstalling
|
## Installing / Uninstalling
|
||||||
@ -22,7 +20,7 @@ The cutest Discord client mod
|
|||||||
|
|
||||||
[](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
[](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
||||||
|
|
||||||
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
|
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
|
## Building from Source
|
||||||
|
|
||||||
@ -41,8 +39,3 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS
|
|||||||
[join]: https://discord.gg/D9uwnFnqmd
|
[join]: https://discord.gg/D9uwnFnqmd
|
||||||
|
|
||||||
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
Discord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.
|
|
||||||
Mention of it does not imply any affiliation with or endorsement by Discord Inc.
|
|
||||||
|
@ -92,7 +92,6 @@ function GM_fetch(url, opt) {
|
|||||||
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||||
resp.text = () => blobTo("text", blob);
|
resp.text = () => blobTo("text", blob);
|
||||||
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||||
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
|
||||||
resolve(resp);
|
resolve(resp);
|
||||||
};
|
};
|
||||||
options.ontimeout = () => reject("fetch timeout");
|
options.ontimeout = () => reject("fetch timeout");
|
||||||
|
@ -13,6 +13,12 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
|||||||
- [Installing Vencord](#installing-vencord)
|
- [Installing Vencord](#installing-vencord)
|
||||||
- [Updating Vencord](#updating-vencord)
|
- [Updating Vencord](#updating-vencord)
|
||||||
- [Uninstalling Vencord](#uninstalling-vencord)
|
- [Uninstalling Vencord](#uninstalling-vencord)
|
||||||
|
- [Manually Installing Vencord](#manually-installing-vencord)
|
||||||
|
- [On Windows](#on-windows)
|
||||||
|
- [On Linux](#on-linux)
|
||||||
|
- [On MacOS](#on-macos)
|
||||||
|
- [Manual Patching](#manual-patching)
|
||||||
|
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@ -21,16 +27,16 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
|||||||
|
|
||||||
## Installing Vencord
|
## Installing Vencord
|
||||||
|
|
||||||
|
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
|
||||||
|
|
||||||
Install `pnpm`:
|
Install `pnpm`:
|
||||||
|
|
||||||
> :exclamation: This next command may need to be run as admin/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
|
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i -g pnpm
|
npm i -g pnpm
|
||||||
```
|
```
|
||||||
|
|
||||||
> :exclamation: **IMPORTANT** Make sure you aren't using an admin/root terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall.
|
|
||||||
|
|
||||||
Clone Vencord:
|
Clone Vencord:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@ -95,4 +101,102 @@ Simply run:
|
|||||||
pnpm uninject
|
pnpm uninject
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The above command may ask you to also run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm uninject
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manually Installing Vencord
|
||||||
|
|
||||||
|
- [Windows](#on-windows)
|
||||||
|
- [Linux](#on-linux)
|
||||||
|
- [MacOS](#on-macos)
|
||||||
|
|
||||||
|
### On Windows
|
||||||
|
|
||||||
|
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### On Linux
|
||||||
|
|
||||||
|
The Discord folder is usually in one of the following paths:
|
||||||
|
|
||||||
|
- /usr/share
|
||||||
|
- /usr/lib64
|
||||||
|
- /opt
|
||||||
|
- /home/$USER/.local/share
|
||||||
|
|
||||||
|
If you use flatpak, it will usually be in one of the following paths:
|
||||||
|
|
||||||
|
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
|
||||||
|
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
|
||||||
|
|
||||||
|
You will need to give flatpak access to vencord with one of the following commands:
|
||||||
|
|
||||||
|
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
|
||||||
|
|
||||||
|
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
|
||||||
|
|
||||||
|
If Discord flatpak install is in /home/:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
|
||||||
|
```
|
||||||
|
|
||||||
|
If Discord flatpak install not in /home/:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### On MacOS
|
||||||
|
|
||||||
|
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
|
||||||
|
|
||||||
|
Go to the `Contents/Resources` folder.
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### Manual Patching
|
||||||
|
|
||||||
|
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
|
||||||
|
|
||||||
|
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
|
||||||
|
|
||||||
|
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
|
||||||
|
|
||||||
|
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
|
||||||
|
|
||||||
|
Make a new folder in `resources` called `app`. In here, we will make two files:
|
||||||
|
|
||||||
|
`package.json` and `index.js`
|
||||||
|
|
||||||
|
In `index.js`:
|
||||||
|
|
||||||
|
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
|
||||||
|
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
|
||||||
|
|
||||||
|
```js
|
||||||
|
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
||||||
|
```
|
||||||
|
|
||||||
|
And in `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "discord", "main": "index.js" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
|
||||||
|
|
||||||
|
### Manually Uninstalling Vencord
|
||||||
|
|
||||||
|
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
|
||||||
|
|
||||||
|
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
|
||||||
|
|
||||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
||||||
|
@ -26,10 +26,6 @@ export default definePlugin({
|
|||||||
name: "Your Name",
|
name: "Your Name",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// Delete `patches` if you are not using code patches, as it will make
|
|
||||||
// your plugin require restarts, and your stop() method will not be
|
|
||||||
// invoked at all. The presence of the key in the object alone is
|
|
||||||
// enough to trigger this behavior, even if the value is an empty array.
|
|
||||||
patches: [],
|
patches: [],
|
||||||
// Delete these two below if you are only using code patches
|
// Delete these two below if you are only using code patches
|
||||||
start() {},
|
start() {},
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.1.4",
|
"version": "1.0.6",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
|
"keywords": [],
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Vendicated/Vencord/issues"
|
"url": "https://github.com/Vendicated/Vencord/issues"
|
||||||
@ -19,7 +20,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build/build.mjs",
|
"build": "node scripts/build/build.mjs",
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
|
||||||
"inject": "node scripts/runInstaller.mjs",
|
"inject": "node scripts/runInstaller.mjs",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"lint-styles": "stylelint \"src/**/*.css\"",
|
"lint-styles": "stylelint \"src/**/*.css\"",
|
||||||
@ -33,13 +33,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vap/core": "0.0.12",
|
"@vap/core": "0.0.12",
|
||||||
"@vap/shiki": "0.10.3",
|
"@vap/shiki": "0.10.3",
|
||||||
"fflate": "^0.7.4",
|
"fflate": "^0.7.4"
|
||||||
"nanoid": "^4.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^5.0.2",
|
"@types/diff": "^5.0.2",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/nanoid": "^3.0.0",
|
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/react": "^18.0.27",
|
"@types/react": "^18.0.27",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
@ -61,7 +59,6 @@
|
|||||||
"standalone-electron-types": "^1.0.0",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"stylelint": "^14.16.1",
|
"stylelint": "^14.16.1",
|
||||||
"stylelint-config-standard": "^29.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"tsx": "^3.12.6",
|
|
||||||
"type-fest": "^3.5.3",
|
"type-fest": "^3.5.3",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
|
306
pnpm-lock.yaml
generated
306
pnpm-lock.yaml
generated
@ -11,7 +11,6 @@ patchedDependencies:
|
|||||||
specifiers:
|
specifiers:
|
||||||
'@types/diff': ^5.0.2
|
'@types/diff': ^5.0.2
|
||||||
'@types/lodash': ^4.14.191
|
'@types/lodash': ^4.14.191
|
||||||
'@types/nanoid': ^3.0.0
|
|
||||||
'@types/node': ^18.11.18
|
'@types/node': ^18.11.18
|
||||||
'@types/react': ^18.0.27
|
'@types/react': ^18.0.27
|
||||||
'@types/react-dom': ^18.0.10
|
'@types/react-dom': ^18.0.10
|
||||||
@ -32,12 +31,10 @@ specifiers:
|
|||||||
fflate: ^0.7.4
|
fflate: ^0.7.4
|
||||||
highlight.js: 10.6.0
|
highlight.js: 10.6.0
|
||||||
moment: ^2.29.4
|
moment: ^2.29.4
|
||||||
nanoid: ^4.0.2
|
|
||||||
puppeteer-core: ^19.6.0
|
puppeteer-core: ^19.6.0
|
||||||
standalone-electron-types: ^1.0.0
|
standalone-electron-types: ^1.0.0
|
||||||
stylelint: ^14.16.1
|
stylelint: ^14.16.1
|
||||||
stylelint-config-standard: ^29.0.0
|
stylelint-config-standard: ^29.0.0
|
||||||
tsx: ^3.12.6
|
|
||||||
type-fest: ^3.5.3
|
type-fest: ^3.5.3
|
||||||
typescript: ^4.9.4
|
typescript: ^4.9.4
|
||||||
|
|
||||||
@ -45,12 +42,10 @@ dependencies:
|
|||||||
'@vap/core': 0.0.12
|
'@vap/core': 0.0.12
|
||||||
'@vap/shiki': 0.10.3
|
'@vap/shiki': 0.10.3
|
||||||
fflate: 0.7.4
|
fflate: 0.7.4
|
||||||
nanoid: 4.0.2
|
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/diff': 5.0.2
|
'@types/diff': 5.0.2
|
||||||
'@types/lodash': 4.14.191
|
'@types/lodash': 4.14.191
|
||||||
'@types/nanoid': 3.0.0
|
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
'@types/react': 18.0.27
|
'@types/react': 18.0.27
|
||||||
'@types/react-dom': 18.0.10
|
'@types/react-dom': 18.0.10
|
||||||
@ -72,7 +67,6 @@ devDependencies:
|
|||||||
standalone-electron-types: 1.0.0
|
standalone-electron-types: 1.0.0
|
||||||
stylelint: 14.16.1
|
stylelint: 14.16.1
|
||||||
stylelint-config-standard: 29.0.0_stylelint@14.16.1
|
stylelint-config-standard: 29.0.0_stylelint@14.16.1
|
||||||
tsx: 3.12.6
|
|
||||||
type-fest: 3.5.3
|
type-fest: 3.5.3
|
||||||
typescript: 4.9.4
|
typescript: 4.9.4
|
||||||
|
|
||||||
@ -110,27 +104,6 @@ packages:
|
|||||||
postcss-selector-parser: 6.0.11
|
postcss-selector-parser: 6.0.11
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@esbuild-kit/cjs-loader/2.4.2:
|
|
||||||
resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==}
|
|
||||||
dependencies:
|
|
||||||
'@esbuild-kit/core-utils': 3.1.0
|
|
||||||
get-tsconfig: 4.4.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@esbuild-kit/core-utils/3.1.0:
|
|
||||||
resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==}
|
|
||||||
dependencies:
|
|
||||||
esbuild: 0.17.12
|
|
||||||
source-map-support: 0.5.21
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@esbuild-kit/esm-loader/2.5.5:
|
|
||||||
resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==}
|
|
||||||
dependencies:
|
|
||||||
'@esbuild-kit/core-utils': 3.1.0
|
|
||||||
get-tsconfig: 4.4.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@esbuild/android-arm/0.15.18:
|
/@esbuild/android-arm/0.15.18:
|
||||||
resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
|
resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -140,96 +113,6 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/android-arm/0.17.12:
|
|
||||||
resolution: {integrity: sha512-E/sgkvwoIfj4aMAPL2e35VnUJspzVYl7+M1B2cqeubdBhADV4uPon0KCc8p2G+LqSJ6i8ocYPCqY3A4GGq0zkQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [android]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/android-arm64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-WQ9p5oiXXYJ33F2EkE3r0FRDFVpEdcDiwNX3u7Xaibxfx6vQE0Sb8ytrfQsA5WO6kDn6mDfKLh6KrPBjvkk7xA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [android]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/android-x64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-m4OsaCr5gT+se25rFPHKQXARMyAehHTQAz4XX1Vk3d27VtqiX0ALMBPoXZsGaB6JYryCLfgGwUslMqTfqeLU0w==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [android]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/darwin-arm64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-O3GCZghRIx+RAN0NDPhyyhRgwa19MoKlzGonIb5hgTj78krqp9XZbYCvFr9N1eUxg0ZQEpiiZ4QvsOQwBpP+lg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/darwin-x64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-5D48jM3tW27h1qjaD9UNRuN+4v0zvksqZSPZqeSWggfMlsVdAhH3pwSfQIFJwcs9QJ9BRibPS4ViZgs3d2wsCA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/freebsd-arm64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-OWvHzmLNTdF1erSvrfoEBGlN94IE6vCEaGEkEH29uo/VoONqPnoDFfShi41Ew+yKimx4vrmmAJEGNoyyP+OgOQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [freebsd]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/freebsd-x64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-A0Xg5CZv8MU9xh4a+7NUpi5VHBKh1RaGJKqjxe4KG87X+mTjDE6ZvlJqpWoeJxgfXHT7IMP9tDFu7IZ03OtJAw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [freebsd]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-arm/0.17.12:
|
|
||||||
resolution: {integrity: sha512-WsHyJ7b7vzHdJ1fv67Yf++2dz3D726oO3QCu8iNYik4fb5YuuReOI9OtA+n7Mk0xyQivNTPbl181s+5oZ38gyA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-arm64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-cK3AjkEc+8v8YG02hYLQIQlOznW+v9N+OI9BAFuyqkfQFR+DnDLhEM5N8QRxAUz99cJTo1rLNXqRrvY15gbQUg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-ia32/0.17.12:
|
|
||||||
resolution: {integrity: sha512-jdOBXJqcgHlah/nYHnj3Hrnl9l63RjtQ4vn9+bohjQPI2QafASB5MtHAoEv0JQHVb/xYQTFOeuHnNYE1zF7tYw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [ia32]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-loong64/0.15.18:
|
/@esbuild/linux-loong64/0.15.18:
|
||||||
resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==}
|
resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -239,114 +122,6 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-loong64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-GTOEtj8h9qPKXCyiBBnHconSCV9LwFyx/gv3Phw0pa25qPYjVuuGZ4Dk14bGCfGX3qKF0+ceeQvwmtI+aYBbVA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [loong64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-mips64el/0.17.12:
|
|
||||||
resolution: {integrity: sha512-o8CIhfBwKcxmEENOH9RwmUejs5jFiNoDw7YgS0EJTF6kgPgcqLFjgoc5kDey5cMHRVCIWc6kK2ShUePOcc7RbA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [mips64el]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-ppc64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-biMLH6NR/GR4z+ap0oJYb877LdBpGac8KfZoEnDiBKd7MD/xt8eaw1SFfYRUeMVx519kVkAOL2GExdFmYnZx3A==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [ppc64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-riscv64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-jkphYUiO38wZGeWlfIBMB72auOllNA2sLfiZPGDtOBb1ELN8lmqBrlMiucgL8awBw1zBXN69PmZM6g4yTX84TA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [riscv64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-s390x/0.17.12:
|
|
||||||
resolution: {integrity: sha512-j3ucLdeY9HBcvODhCY4b+Ds3hWGO8t+SAidtmWu/ukfLLG/oYDMaA+dnugTVAg5fnUOGNbIYL9TOjhWgQB8W5g==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [s390x]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/linux-x64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-uo5JL3cgaEGotaqSaJdRfFNSCUJOIliKLnDGWaVCgIKkHxwhYMm95pfMbWZ9l7GeW9kDg0tSxcy9NYdEtjwwmA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/netbsd-x64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-DNdoRg8JX+gGsbqt2gPgkgb00mqOgOO27KnrWZtdABl6yWTST30aibGJ6geBq3WM2TIeW6COs5AScnC7GwtGPg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [netbsd]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/openbsd-x64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-aVsENlr7B64w8I1lhHShND5o8cW6sB9n9MUtLumFlPhG3elhNWtE7M1TFpj3m7lT3sKQUMkGFjTQBrvDDO1YWA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [openbsd]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/sunos-x64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-qbHGVQdKSwi0JQJuZznS4SyY27tYXYF0mrgthbxXrZI3AHKuRvU+Eqbg/F0rmLDpW/jkIZBlCO1XfHUBMNJ1pg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [sunos]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/win32-arm64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-zsCp8Ql+96xXTVTmm6ffvoTSZSV2B/LzzkUXAY33F/76EajNw1m+jZ9zPfNJlJ3Rh4EzOszNDHsmG/fZOhtqDg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/win32-ia32/0.17.12:
|
|
||||||
resolution: {integrity: sha512-FfrFjR4id7wcFYOdqbDfDET3tjxCozUgbqdkOABsSFzoZGFC92UK7mg4JKRc/B3NNEf1s2WHxJ7VfTdVDPN3ng==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [ia32]
|
|
||||||
os: [win32]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@esbuild/win32-x64/0.17.12:
|
|
||||||
resolution: {integrity: sha512-JOOxw49BVZx2/5tW3FqkdjSD/5gXYeVGPDcB0lvap0gLQshkh1Nyel1QazC+wNxus3xPlsYAgqU1BUmrmCvWtw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@eslint/eslintrc/1.3.3:
|
/@eslint/eslintrc/1.3.3:
|
||||||
resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==}
|
resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@ -421,13 +196,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/nanoid/3.0.0:
|
|
||||||
resolution: {integrity: sha512-UXitWSmXCwhDmAKe7D3hNQtQaHeHt5L8LO1CB8GF8jlYVzOv5cBWDNqiJ+oPEWrWei3i3dkZtHY/bUtd0R/uOQ==}
|
|
||||||
deprecated: This is a stub types definition. nanoid provides its own type definitions, so you do not need this installed.
|
|
||||||
dependencies:
|
|
||||||
nanoid: 4.0.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/node/18.11.18:
|
/@types/node/18.11.18:
|
||||||
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
|
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -795,10 +563,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/buffer-from/1.1.2:
|
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/buffer/5.7.1:
|
/buffer/5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1283,36 +1047,6 @@ packages:
|
|||||||
esbuild-windows-arm64: 0.15.18
|
esbuild-windows-arm64: 0.15.18
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/esbuild/0.17.12:
|
|
||||||
resolution: {integrity: sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
hasBin: true
|
|
||||||
requiresBuild: true
|
|
||||||
optionalDependencies:
|
|
||||||
'@esbuild/android-arm': 0.17.12
|
|
||||||
'@esbuild/android-arm64': 0.17.12
|
|
||||||
'@esbuild/android-x64': 0.17.12
|
|
||||||
'@esbuild/darwin-arm64': 0.17.12
|
|
||||||
'@esbuild/darwin-x64': 0.17.12
|
|
||||||
'@esbuild/freebsd-arm64': 0.17.12
|
|
||||||
'@esbuild/freebsd-x64': 0.17.12
|
|
||||||
'@esbuild/linux-arm': 0.17.12
|
|
||||||
'@esbuild/linux-arm64': 0.17.12
|
|
||||||
'@esbuild/linux-ia32': 0.17.12
|
|
||||||
'@esbuild/linux-loong64': 0.17.12
|
|
||||||
'@esbuild/linux-mips64el': 0.17.12
|
|
||||||
'@esbuild/linux-ppc64': 0.17.12
|
|
||||||
'@esbuild/linux-riscv64': 0.17.12
|
|
||||||
'@esbuild/linux-s390x': 0.17.12
|
|
||||||
'@esbuild/linux-x64': 0.17.12
|
|
||||||
'@esbuild/netbsd-x64': 0.17.12
|
|
||||||
'@esbuild/openbsd-x64': 0.17.12
|
|
||||||
'@esbuild/sunos-x64': 0.17.12
|
|
||||||
'@esbuild/win32-arm64': 0.17.12
|
|
||||||
'@esbuild/win32-ia32': 0.17.12
|
|
||||||
'@esbuild/win32-x64': 0.17.12
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/escape-string-regexp/1.0.5:
|
/escape-string-regexp/1.0.5:
|
||||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||||
engines: {node: '>=0.8.0'}
|
engines: {node: '>=0.8.0'}
|
||||||
@ -1666,14 +1400,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/fsevents/2.3.2:
|
|
||||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
|
||||||
os: [darwin]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/function-bind/1.1.1:
|
/function-bind/1.1.1:
|
||||||
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1685,10 +1411,6 @@ packages:
|
|||||||
pump: 3.0.0
|
pump: 3.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/get-tsconfig/4.4.0:
|
|
||||||
resolution: {integrity: sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/get-value/2.0.6:
|
/get-value/2.0.6:
|
||||||
resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==}
|
resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -2256,11 +1978,6 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/nanoid/4.0.2:
|
|
||||||
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
|
||||||
engines: {node: ^14 || ^16 || >=18}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
/nanomatch/1.2.13:
|
/nanomatch/1.2.13:
|
||||||
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
|
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -2750,13 +2467,6 @@ packages:
|
|||||||
urix: 0.1.0
|
urix: 0.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/source-map-support/0.5.21:
|
|
||||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
|
||||||
dependencies:
|
|
||||||
buffer-from: 1.1.2
|
|
||||||
source-map: 0.6.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/source-map-url/0.4.1:
|
/source-map-url/0.4.1:
|
||||||
resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==}
|
resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==}
|
||||||
deprecated: See https://github.com/lydell/source-map-url#deprecated
|
deprecated: See https://github.com/lydell/source-map-url#deprecated
|
||||||
@ -2767,11 +2477,6 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/source-map/0.6.1:
|
|
||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/spdx-correct/3.1.1:
|
/spdx-correct/3.1.1:
|
||||||
resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
|
resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3034,17 +2739,6 @@ packages:
|
|||||||
typescript: 4.9.4
|
typescript: 4.9.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/tsx/3.12.6:
|
|
||||||
resolution: {integrity: sha512-q93WgS3lBdHlPgS0h1i+87Pt6n9K/qULIMNYZo07nSeu2z5QE2CellcAZfofVXBo2tQg9av2ZcRMQ2S2i5oadQ==}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
'@esbuild-kit/cjs-loader': 2.4.2
|
|
||||||
'@esbuild-kit/core-utils': 3.1.0
|
|
||||||
'@esbuild-kit/esm-loader': 2.5.5
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents: 2.3.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/type-check/0.4.0:
|
/type-check/0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
@ -36,7 +36,7 @@ const commonOptions = {
|
|||||||
entryPoints: ["browser/Vencord.ts"],
|
entryPoints: ["browser/Vencord.ts"],
|
||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
format: "iife",
|
format: "iife",
|
||||||
external: ["plugins", "git-hash", "/assets/*"],
|
external: ["plugins", "git-hash"],
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins,
|
globPlugins,
|
||||||
...commonOpts.plugins,
|
...commonOpts.plugins,
|
||||||
|
@ -33,8 +33,6 @@ export const banner = {
|
|||||||
`.trim()
|
`.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
|
||||||
|
|
||||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
@ -71,15 +69,9 @@ export const globPlugins = {
|
|||||||
const files = await readdir(`./src/${dir}`);
|
const files = await readdir(`./src/${dir}`);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.startsWith(".")) continue;
|
if (file.startsWith(".")) continue;
|
||||||
if (file === "index.ts") continue;
|
if (file === "index.ts") {
|
||||||
const fileBits = file.split(".");
|
continue;
|
||||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
|
|
||||||
const mod = fileBits.at(-2);
|
|
||||||
if (mod === "dev" && !watch) continue;
|
|
||||||
if (mod === "web" && !isWeb) continue;
|
|
||||||
if (mod === "desktop" && isWeb) continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mod = `p${i}`;
|
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`;
|
plugins += `[${mod}.name]:${mod},\n`;
|
||||||
@ -193,7 +185,7 @@ export const commonOpts = {
|
|||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
banner,
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
external: ["~plugins", "~git-hash", "~git-remote"],
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
inject: ["./scripts/build/inject/react.mjs"],
|
||||||
jsxFactory: "VencordCreateElement",
|
jsxFactory: "VencordCreateElement",
|
||||||
jsxFragment: "VencordFragment",
|
jsxFragment: "VencordFragment",
|
||||||
|
@ -1,191 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
||||||
import { access, readFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
|
||||||
|
|
||||||
interface Dev {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginData {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
authors: Dev[];
|
|
||||||
dependencies: string[];
|
|
||||||
hasPatches: boolean;
|
|
||||||
hasCommands: boolean;
|
|
||||||
required: boolean;
|
|
||||||
enabledByDefault: boolean;
|
|
||||||
target: "desktop" | "web" | "dev";
|
|
||||||
}
|
|
||||||
|
|
||||||
const devs = {} as Record<string, Dev>;
|
|
||||||
|
|
||||||
function getName(node: NamedDeclaration) {
|
|
||||||
return node.name && isIdentifier(node.name) ? node.name.text : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasName(node: NamedDeclaration, name: string) {
|
|
||||||
return getName(node) === name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getObjectProp(node: ObjectLiteralExpression, name: string) {
|
|
||||||
const prop = node.properties.find(p => hasName(p, name));
|
|
||||||
if (prop && isPropertyAssignment(prop)) return prop.initializer;
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDevs() {
|
|
||||||
const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest);
|
|
||||||
|
|
||||||
for (const child of file.getChildAt(0).getChildren()) {
|
|
||||||
if (!isVariableStatement(child)) continue;
|
|
||||||
|
|
||||||
const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "Devs"));
|
|
||||||
if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue;
|
|
||||||
|
|
||||||
const value = devsDeclaration.initializer.arguments[0];
|
|
||||||
|
|
||||||
if (!isObjectLiteralExpression(value)) return;
|
|
||||||
|
|
||||||
for (const prop of value.properties) {
|
|
||||||
const name = (prop.name as Identifier).text;
|
|
||||||
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
|
||||||
|
|
||||||
if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`);
|
|
||||||
|
|
||||||
devs[name] = {
|
|
||||||
name: (getObjectProp(value, "name") as StringLiteral).text,
|
|
||||||
id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Could not find Devs constant");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function parseFile(fileName: string) {
|
|
||||||
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
|
|
||||||
|
|
||||||
const fail = (reason: string) => {
|
|
||||||
return new Error(`Invalid plugin ${fileName}, because ${reason}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const node of file.getChildAt(0).getChildren()) {
|
|
||||||
if (!isExportAssignment(node) || !isCallExpression(node.expression)) continue;
|
|
||||||
|
|
||||||
const call = node.expression;
|
|
||||||
if (!isIdentifier(call.expression) || call.expression.text !== "definePlugin") continue;
|
|
||||||
|
|
||||||
const pluginObj = node.expression.arguments[0];
|
|
||||||
if (!isObjectLiteralExpression(pluginObj)) throw fail("no object literal passed to definePlugin");
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
hasPatches: false,
|
|
||||||
hasCommands: false,
|
|
||||||
enabledByDefault: false,
|
|
||||||
required: false,
|
|
||||||
} as PluginData;
|
|
||||||
|
|
||||||
for (const prop of pluginObj.properties) {
|
|
||||||
const key = getName(prop);
|
|
||||||
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case "name":
|
|
||||||
case "description":
|
|
||||||
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
|
|
||||||
data[key] = value.text;
|
|
||||||
break;
|
|
||||||
case "patches":
|
|
||||||
data.hasPatches = true;
|
|
||||||
break;
|
|
||||||
case "commands":
|
|
||||||
data.hasCommands = true;
|
|
||||||
break;
|
|
||||||
case "authors":
|
|
||||||
if (!isArrayLiteralExpression(value)) throw fail("authors is not an array literal");
|
|
||||||
data.authors = value.elements.map(e => {
|
|
||||||
if (!isPropertyAccessExpression(e)) throw fail("authors array contains non-property access expressions");
|
|
||||||
return devs[getName(e)!];
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "dependencies":
|
|
||||||
if (!isArrayLiteralExpression(value)) throw fail("dependencies is not an array literal");
|
|
||||||
const { elements } = value;
|
|
||||||
if (elements.some(e => !isStringLiteral(e))) throw fail("dependencies array contains non-string elements");
|
|
||||||
data.dependencies = (elements as NodeArray<StringLiteral>).map(e => e.text);
|
|
||||||
break;
|
|
||||||
case "required":
|
|
||||||
case "enabledByDefault":
|
|
||||||
data[key] = value.kind === SyntaxKind.TrueKeyword;
|
|
||||||
if (!data[key] && value.kind !== SyntaxKind.FalseKeyword) throw fail(`${key} is not a boolean literal`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
|
|
||||||
|
|
||||||
const fileBits = fileName.split(".");
|
|
||||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
|
|
||||||
const mod = fileBits.at(-2)!;
|
|
||||||
if (!["web", "desktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
|
|
||||||
data.target = mod as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw fail("no default export called 'definePlugin' found");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEntryPoint(dirent: Dirent) {
|
|
||||||
const base = join("./src/plugins", dirent.name);
|
|
||||||
if (!dirent.isDirectory()) return base;
|
|
||||||
|
|
||||||
for (const name of ["index.ts", "index.tsx"]) {
|
|
||||||
const full = join(base, name);
|
|
||||||
try {
|
|
||||||
await access(full);
|
|
||||||
return full;
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`${dirent.name}: Couldn't find entry point`);
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
parseDevs();
|
|
||||||
const plugins = readdirSync("./src/plugins", { withFileTypes: true }).filter(d => d.name !== "index.ts");
|
|
||||||
|
|
||||||
const promises = plugins.map(async dirent => parseFile(await getEntryPoint(dirent)));
|
|
||||||
|
|
||||||
const data = JSON.stringify(await Promise.all(promises));
|
|
||||||
|
|
||||||
if (process.argv.length > 2) {
|
|
||||||
writeFileSync(process.argv[2], data);
|
|
||||||
} else {
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
})();
|
|
@ -27,7 +27,7 @@ export { PlainSettings, Settings };
|
|||||||
import "./utils/quickCss";
|
import "./utils/quickCss";
|
||||||
import "./webpack/patchWebpack";
|
import "./webpack/patchWebpack";
|
||||||
|
|
||||||
import { showNotification } from "./api/Notifications";
|
import { popNotice, showNotice } from "./api/Notices";
|
||||||
import { PlainSettings, Settings } from "./api/settings";
|
import { PlainSettings, Settings } from "./api/settings";
|
||||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||||
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
||||||
@ -49,32 +49,32 @@ async function init() {
|
|||||||
if (Settings.autoUpdate) {
|
if (Settings.autoUpdate) {
|
||||||
await update();
|
await update();
|
||||||
const needsFullRestart = await rebuild();
|
const needsFullRestart = await rebuild();
|
||||||
if (Settings.autoUpdateNotification)
|
setTimeout(() => {
|
||||||
setTimeout(() => showNotification({
|
showNotice(
|
||||||
title: "Vencord has been updated!",
|
"Vencord has been updated!",
|
||||||
body: "Click here to restart",
|
"Restart",
|
||||||
permanent: true,
|
() => {
|
||||||
noPersist: true,
|
|
||||||
onClick() {
|
|
||||||
if (needsFullRestart)
|
if (needsFullRestart)
|
||||||
window.DiscordNative.app.relaunch();
|
window.DiscordNative.app.relaunch();
|
||||||
else
|
else
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
}), 10_000);
|
);
|
||||||
|
}, 10_000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.notifyAboutUpdates)
|
if (Settings.notifyAboutUpdates)
|
||||||
setTimeout(() => showNotification({
|
setTimeout(() => {
|
||||||
title: "A Vencord update is available!",
|
showNotice(
|
||||||
body: "Click here to view the update",
|
"A Vencord update is available!",
|
||||||
permanent: true,
|
"View Update",
|
||||||
noPersist: true,
|
() => {
|
||||||
onClick() {
|
popNotice();
|
||||||
SettingsRouter.open("VencordUpdater");
|
SettingsRouter.open("VencordUpdater");
|
||||||
}
|
}
|
||||||
}), 10_000);
|
);
|
||||||
|
}, 10_000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
@ -95,12 +95,3 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
document.head.append(Object.assign(document.createElement("style"), {
|
|
||||||
id: "vencord-native-titlebar-style",
|
|
||||||
textContent: "[class*=titleBar-]{display: none!important}"
|
|
||||||
}));
|
|
||||||
}, { once: true });
|
|
||||||
}
|
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 Logger from "@utils/Logger";
|
|
||||||
import type { ReactElement } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param children The rendered context menu elements
|
|
||||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
|
||||||
*/
|
|
||||||
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => void;
|
|
||||||
/**
|
|
||||||
* @param The navId of the context menu being patched
|
|
||||||
* @param children The rendered context menu elements
|
|
||||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
|
||||||
*/
|
|
||||||
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => void;
|
|
||||||
|
|
||||||
const ContextMenuLogger = new Logger("ContextMenu");
|
|
||||||
|
|
||||||
export const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();
|
|
||||||
export const globalPatches = new Set<GlobalContextMenuPatchCallback>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a context menu patch
|
|
||||||
* @param navId The navId(s) for the context menu(s) to patch
|
|
||||||
* @param patch The patch to be applied
|
|
||||||
*/
|
|
||||||
export function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {
|
|
||||||
if (!Array.isArray(navId)) navId = [navId];
|
|
||||||
for (const id of navId) {
|
|
||||||
let contextMenuPatches = navPatches.get(id);
|
|
||||||
if (!contextMenuPatches) {
|
|
||||||
contextMenuPatches = new Set();
|
|
||||||
navPatches.set(id, contextMenuPatches);
|
|
||||||
}
|
|
||||||
|
|
||||||
contextMenuPatches.add(patch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a global context menu patch that fires the patch for all context menus
|
|
||||||
* @param patch The patch to be applied
|
|
||||||
*/
|
|
||||||
export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {
|
|
||||||
globalPatches.add(patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a context menu patch
|
|
||||||
* @param navId The navId(s) for the context menu(s) to remove the patch
|
|
||||||
* @param patch The patch to be removed
|
|
||||||
* @returns Wheter the patch was sucessfully removed from the context menu(s)
|
|
||||||
*/
|
|
||||||
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
|
|
||||||
const navIds = Array.isArray(navId) ? navId : [navId as string];
|
|
||||||
|
|
||||||
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
|
|
||||||
|
|
||||||
return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a global context menu patch
|
|
||||||
* @returns Wheter the patch was sucessfully removed
|
|
||||||
*/
|
|
||||||
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
|
|
||||||
return globalPatches.delete(patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs
|
|
||||||
* @param id The id of the child
|
|
||||||
*/
|
|
||||||
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null {
|
|
||||||
for (const child of children) {
|
|
||||||
if (child == null) continue;
|
|
||||||
|
|
||||||
if (child.props?.id === id) return itemsArray ?? null;
|
|
||||||
|
|
||||||
let nextChildren = child.props?.children;
|
|
||||||
if (nextChildren) {
|
|
||||||
if (!Array.isArray(nextChildren)) {
|
|
||||||
nextChildren = [nextChildren];
|
|
||||||
child.props.children = nextChildren;
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
|
|
||||||
if (found !== null) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextMenuProps {
|
|
||||||
contextMenuApiArguments?: Array<any>;
|
|
||||||
navId: string;
|
|
||||||
children: Array<ReactElement>;
|
|
||||||
"aria-label": string;
|
|
||||||
onSelect: (() => void) | undefined;
|
|
||||||
onClose: (callback: (...args: Array<any>) => any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function _patchContextMenu(props: ContextMenuProps) {
|
|
||||||
props.contextMenuApiArguments ??= [];
|
|
||||||
const contextMenuPatches = navPatches.get(props.navId);
|
|
||||||
|
|
||||||
if (!Array.isArray(props.children)) props.children = [props.children];
|
|
||||||
|
|
||||||
if (contextMenuPatches) {
|
|
||||||
for (const patch of contextMenuPatches) {
|
|
||||||
try {
|
|
||||||
patch(props.children, ...props.contextMenuApiArguments);
|
|
||||||
} catch (err) {
|
|
||||||
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const patch of globalPatches) {
|
|
||||||
try {
|
|
||||||
patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
|
||||||
} catch (err) {
|
|
||||||
ContextMenuLogger.error("Global patch errored,", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -19,7 +19,6 @@
|
|||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { MessageStore } from "@webpack/common";
|
import { MessageStore } from "@webpack/common";
|
||||||
import type { Channel, Message } from "discord-types/general";
|
import type { Channel, Message } from "discord-types/general";
|
||||||
import type { Promisable } from "type-fest";
|
|
||||||
|
|
||||||
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
||||||
|
|
||||||
@ -42,16 +41,16 @@ export interface MessageExtra {
|
|||||||
stickerIds?: string[];
|
stickerIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
|
||||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
|
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
||||||
|
|
||||||
const sendListeners = new Set<SendListener>();
|
const sendListeners = new Set<SendListener>();
|
||||||
const editListeners = new Set<EditListener>();
|
const editListeners = new Set<EditListener>();
|
||||||
|
|
||||||
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
const result = await listener(channelId, messageObj, extra);
|
const result = listener(channelId, messageObj, extra);
|
||||||
if (result && result.cancel === true) {
|
if (result && result.cancel === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -62,10 +61,10 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
||||||
for (const listener of editListeners) {
|
for (const listener of editListeners) {
|
||||||
try {
|
try {
|
||||||
await listener(channelId, messageId, messageObj);
|
listener(channelId, messageId, messageObj);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,7 @@ import "./styles.css";
|
|||||||
|
|
||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { classes } from "@utils/misc";
|
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
||||||
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
|
||||||
|
|
||||||
import { NotificationData } from "./Notifications";
|
import { NotificationData } from "./Notifications";
|
||||||
|
|
||||||
@ -33,11 +32,8 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
icon,
|
icon,
|
||||||
onClick,
|
onClick,
|
||||||
onClose,
|
onClose,
|
||||||
image,
|
image
|
||||||
permanent,
|
}: NotificationData) {
|
||||||
className,
|
|
||||||
dismissOnClick
|
|
||||||
}: NotificationData & { className?: string; }) {
|
|
||||||
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
||||||
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
||||||
|
|
||||||
@ -47,7 +43,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isHover || !hasFocus || timeout === 0 || permanent) return void setElapsed(0);
|
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
const elapsed = Date.now() - start;
|
const elapsed = Date.now() - start;
|
||||||
@ -64,13 +60,9 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classes("vc-notification-root", className)}
|
className="vc-notification-root"
|
||||||
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
||||||
onClick={() => {
|
onClick={onClick}
|
||||||
onClick?.();
|
|
||||||
if (dismissOnClick !== false)
|
|
||||||
onClose!();
|
|
||||||
}}
|
|
||||||
onContextMenu={e => {
|
onContextMenu={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -82,35 +74,14 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
<div className="vc-notification">
|
<div className="vc-notification">
|
||||||
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
||||||
<div className="vc-notification-content">
|
<div className="vc-notification-content">
|
||||||
<div className="vc-notification-header">
|
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
|
||||||
<h2 className="vc-notification-title">{title}</h2>
|
|
||||||
<button
|
|
||||||
className="vc-notification-close-btn"
|
|
||||||
onClick={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onClose!();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
role="img"
|
|
||||||
aria-labelledby="vc-notification-dismiss-title"
|
|
||||||
>
|
|
||||||
<title id="vc-notification-dismiss-title">Dismiss Notification</title>
|
|
||||||
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{image && <img className="vc-notification-img" src={image} alt="" />}
|
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||||
{timeout !== 0 && !permanent && (
|
{timeout !== 0 && (
|
||||||
<div
|
<div
|
||||||
className="vc-notification-progressbar"
|
className="vc-notification-progressbar"
|
||||||
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
||||||
@ -118,6 +89,4 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}, {
|
|
||||||
onError: ({ props }) => props.onClose!()
|
|
||||||
});
|
});
|
||||||
|
@ -23,7 +23,6 @@ import type { ReactNode } from "react";
|
|||||||
import type { Root } from "react-dom/client";
|
import type { Root } from "react-dom/client";
|
||||||
|
|
||||||
import NotificationComponent from "./NotificationComponent";
|
import NotificationComponent from "./NotificationComponent";
|
||||||
import { persistNotification } from "./notificationLog";
|
|
||||||
|
|
||||||
const NotificationQueue = new Queue();
|
const NotificationQueue = new Queue();
|
||||||
|
|
||||||
@ -55,12 +54,6 @@ export interface NotificationData {
|
|||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
onClose?(): void;
|
onClose?(): void;
|
||||||
color?: string;
|
color?: string;
|
||||||
/** Whether this notification should not have a timeout */
|
|
||||||
permanent?: boolean;
|
|
||||||
/** Whether this notification should not be persisted in the Notification Log */
|
|
||||||
noPersist?: boolean;
|
|
||||||
/** Whether this notification should be dismissed when clicked (defaults to true) */
|
|
||||||
dismissOnClick?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showNotification(notification: NotificationData, id: number) {
|
function _showNotification(notification: NotificationData, id: number) {
|
||||||
@ -91,8 +84,6 @@ export async function requestPermission() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function showNotification(data: NotificationData) {
|
export async function showNotification(data: NotificationData) {
|
||||||
persistNotification(data);
|
|
||||||
|
|
||||||
if (shouldBeNative() && await requestPermission()) {
|
if (shouldBeNative() && await requestPermission()) {
|
||||||
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
||||||
const n = new Notification(title, {
|
const n = new Notification(title, {
|
||||||
|
@ -1,203 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 * as DataStore from "@api/DataStore";
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
|
||||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
||||||
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import type { DispatchWithoutAction } from "react";
|
|
||||||
|
|
||||||
import NotificationComponent from "./NotificationComponent";
|
|
||||||
import type { NotificationData } from "./Notifications";
|
|
||||||
|
|
||||||
interface PersistentNotificationData extends Pick<NotificationData, "title" | "body" | "image" | "icon" | "color"> {
|
|
||||||
timestamp: number;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KEY = "notification-log";
|
|
||||||
|
|
||||||
const getLog = async () => {
|
|
||||||
const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;
|
|
||||||
return log ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-notification-log-");
|
|
||||||
const signals = new Set<DispatchWithoutAction>();
|
|
||||||
|
|
||||||
export async function persistNotification(notification: NotificationData) {
|
|
||||||
if (notification.noPersist) return;
|
|
||||||
|
|
||||||
const limit = Settings.notifications.logLimit;
|
|
||||||
if (limit === 0) return;
|
|
||||||
|
|
||||||
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
|
|
||||||
const log = old ?? [];
|
|
||||||
|
|
||||||
// Omit stuff we don't need
|
|
||||||
const {
|
|
||||||
onClick, onClose, richBody, permanent, noPersist, dismissOnClick,
|
|
||||||
...pureNotification
|
|
||||||
} = notification;
|
|
||||||
|
|
||||||
log.unshift({
|
|
||||||
...pureNotification,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
id: nanoid()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (log.length > limit && limit !== 200)
|
|
||||||
log.length = limit;
|
|
||||||
|
|
||||||
return log;
|
|
||||||
});
|
|
||||||
|
|
||||||
signals.forEach(x => x());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteNotification(timestamp: number) {
|
|
||||||
const log = await getLog();
|
|
||||||
const index = log.findIndex(x => x.timestamp === timestamp);
|
|
||||||
if (index === -1) return;
|
|
||||||
|
|
||||||
log.splice(index, 1);
|
|
||||||
await DataStore.set(KEY, log);
|
|
||||||
signals.forEach(x => x());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLogs() {
|
|
||||||
const [signal, setSignal] = useReducer(x => x + 1, 0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
signals.add(setSignal);
|
|
||||||
return () => void signals.delete(setSignal);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [log, _, pending] = useAwaiter(getLog, {
|
|
||||||
fallbackValue: [],
|
|
||||||
deps: [signal]
|
|
||||||
});
|
|
||||||
|
|
||||||
return [log, pending] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|
||||||
const [removing, setRemoving] = useState(false);
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const div = ref.current!;
|
|
||||||
|
|
||||||
const setHeight = () => {
|
|
||||||
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
|
|
||||||
div.style.height = `${div.clientHeight}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
setHeight();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cl("wrapper", { removing })} ref={ref}>
|
|
||||||
<NotificationComponent
|
|
||||||
{...data}
|
|
||||||
permanent={true}
|
|
||||||
dismissOnClick={false}
|
|
||||||
onClose={() => {
|
|
||||||
if (removing) return;
|
|
||||||
setRemoving(true);
|
|
||||||
|
|
||||||
setTimeout(() => deleteNotification(data.timestamp), 200);
|
|
||||||
}}
|
|
||||||
richBody={
|
|
||||||
<div className={cl("body")}>
|
|
||||||
{data.body}
|
|
||||||
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
|
|
||||||
if (!log.length && !pending)
|
|
||||||
return (
|
|
||||||
<div className={cl("container")}>
|
|
||||||
<div className={cl("empty")} />
|
|
||||||
<Forms.FormText style={{ textAlign: "center" }}>
|
|
||||||
No notifications yet
|
|
||||||
</Forms.FormText>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cl("container")}>
|
|
||||||
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
|
|
||||||
const [log, pending] = useLogs();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
|
|
||||||
<ModalHeader>
|
|
||||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
|
|
||||||
<ModalCloseButton onClick={close} />
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalContent>
|
|
||||||
<NotificationLog log={log} pending={pending} />
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
disabled={log.length === 0}
|
|
||||||
onClick={() => {
|
|
||||||
Alerts.show({
|
|
||||||
title: "Are you sure?",
|
|
||||||
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
|
|
||||||
async onConfirm() {
|
|
||||||
await DataStore.set(KEY, []);
|
|
||||||
signals.forEach(x => x());
|
|
||||||
},
|
|
||||||
confirmText: "Do it!",
|
|
||||||
confirmColor: "vc-notification-log-danger-btn",
|
|
||||||
cancelText: "Nevermind"
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear Notification Log
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalRoot>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openNotificationLogModal() {
|
|
||||||
const key = openModal(modalProps => (
|
|
||||||
<LogModal
|
|
||||||
modalProps={modalProps}
|
|
||||||
close={() => closeModal(key)}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
|
@ -3,20 +3,16 @@
|
|||||||
all: unset;
|
all: unset;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
width: 25vw;
|
||||||
|
min-height: 10vh;
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
width: 25vw;
|
border-radius: 6px;
|
||||||
min-height: 10vh;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification {
|
.vc-notification {
|
||||||
@ -26,42 +22,17 @@
|
|||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-title {
|
|
||||||
color: var(--header-primary);
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-close-btn {
|
|
||||||
all: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--interactive-normal);
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-close-btn:hover {
|
|
||||||
color: var(--interactive-hover);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-icon {
|
.vc-notification-icon {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Discord adding 3km margin to generic tags */
|
||||||
|
.vc-notification h2 {
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.vc-notification-progressbar {
|
.vc-notification-progressbar {
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -76,47 +47,3 @@
|
|||||||
.vc-notification-img {
|
.vc-notification-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-log-empty {
|
|
||||||
height: 218px;
|
|
||||||
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1em;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-wrapper {
|
|
||||||
transition: 200ms ease;
|
|
||||||
transition-property: height, opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-wrapper:not(:last-child) {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-removing {
|
|
||||||
height: 0 !important;
|
|
||||||
opacity: 0;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-timestamp {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-danger-btn {
|
|
||||||
color: var(--white-500);
|
|
||||||
background-color: var(--button-danger-background);
|
|
||||||
}
|
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
import * as $Badges from "./Badges";
|
import * as $Badges from "./Badges";
|
||||||
import * as $Commands from "./Commands";
|
import * as $Commands from "./Commands";
|
||||||
import * as $ContextMenu from "./ContextMenu";
|
|
||||||
import * as $DataStore from "./DataStore";
|
import * as $DataStore from "./DataStore";
|
||||||
import * as $MemberListDecorators from "./MemberListDecorators";
|
import * as $MemberListDecorators from "./MemberListDecorators";
|
||||||
import * as $MessageAccessories from "./MessageAccessories";
|
import * as $MessageAccessories from "./MessageAccessories";
|
||||||
@ -94,8 +93,3 @@ export const Styles = $Styles;
|
|||||||
* An API allowing you to display notifications
|
* An API allowing you to display notifications
|
||||||
*/
|
*/
|
||||||
export const Notifications = $Notifications;
|
export const Notifications = $Notifications;
|
||||||
|
|
||||||
/**
|
|
||||||
* An api allowing you to patch and add/remove items to/from context menus
|
|
||||||
*/
|
|
||||||
export const ContextMenu = $ContextMenu;
|
|
||||||
|
@ -28,14 +28,12 @@ const logger = new Logger("Settings");
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
notifyAboutUpdates: boolean;
|
notifyAboutUpdates: boolean;
|
||||||
autoUpdate: boolean;
|
autoUpdate: boolean;
|
||||||
autoUpdateNotification: boolean,
|
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
enableReactDevtools: boolean;
|
||||||
themeLinks: string[];
|
themeLinks: string[];
|
||||||
frameless: boolean;
|
frameless: boolean;
|
||||||
transparent: boolean;
|
transparent: boolean;
|
||||||
winCtrlQ: boolean;
|
winCtrlQ: boolean;
|
||||||
winNativeTitleBar: boolean;
|
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -47,28 +45,24 @@ export interface Settings {
|
|||||||
timeout: number;
|
timeout: number;
|
||||||
position: "top-right" | "bottom-right";
|
position: "top-right" | "bottom-right";
|
||||||
useNative: "always" | "never" | "not-focused";
|
useNative: "always" | "never" | "not-focused";
|
||||||
logLimit: number;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
notifyAboutUpdates: true,
|
notifyAboutUpdates: true,
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
autoUpdateNotification: true,
|
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
themeLinks: [],
|
themeLinks: [],
|
||||||
enableReactDevtools: false,
|
enableReactDevtools: false,
|
||||||
frameless: false,
|
frameless: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
winCtrlQ: false,
|
winCtrlQ: false,
|
||||||
winNativeTitleBar: false,
|
|
||||||
plugins: {},
|
plugins: {},
|
||||||
|
|
||||||
notifications: {
|
notifications: {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
useNative: "not-focused",
|
useNative: "not-focused"
|
||||||
logLimit: 50
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,7 +90,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
// Return empty for plugins with no settings
|
// Return empty for plugins with no settings
|
||||||
if (path === "plugins" && p in plugins)
|
if (path === "plugins" && p in plugins)
|
||||||
return target[p] = makeProxy({
|
return target[p] = makeProxy({
|
||||||
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
|
enabled: plugins[p].required ?? false
|
||||||
}, root, `plugins.${p}`);
|
}, root, `plugins.${p}`);
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
@ -135,7 +129,6 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
target[p] = v;
|
target[p] = v;
|
||||||
// Call any listeners that are listening to a setting of this path
|
// Call any listeners that are listening to a setting of this path
|
||||||
const setPath = `${path}${path && "."}${p}`;
|
const setPath = `${path}${path && "."}${p}`;
|
||||||
delete proxyCache[setPath];
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
if (!subscription._path || subscription._path === setPath) {
|
if (!subscription._path || subscription._path === setPath) {
|
||||||
subscription(v, setPath);
|
subscription(v, setPath);
|
||||||
@ -172,11 +165,11 @@ export const Settings = makeProxy(settings);
|
|||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
||||||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
export function useSettings(paths?: string[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
const onUpdate: SubscriptionCallback = paths
|
const onUpdate: SubscriptionCallback = paths
|
||||||
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
|
? (value, path) => paths.includes(path) && forceUpdate()
|
||||||
: forceUpdate;
|
: forceUpdate;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -234,7 +227,7 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
|
|||||||
return Settings.plugins[definedSettings.pluginName] as any;
|
return Settings.plugins[definedSettings.pluginName] as any;
|
||||||
},
|
},
|
||||||
use: settings => useSettings(
|
use: settings => useSettings(
|
||||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
|
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`)
|
||||||
).plugins[definedSettings.pluginName] as any,
|
).plugins[definedSettings.pluginName] as any,
|
||||||
def,
|
def,
|
||||||
checks: checks ?? {},
|
checks: checks ?? {},
|
||||||
@ -242,15 +235,3 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
|
|||||||
};
|
};
|
||||||
return definedSettings;
|
return definedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];
|
|
||||||
|
|
||||||
type ResolveUseSettings<T extends object> = {
|
|
||||||
[Key in keyof T]:
|
|
||||||
Key extends string
|
|
||||||
? T[Key] extends Record<string, unknown>
|
|
||||||
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
|
|
||||||
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
|
|
||||||
: Key
|
|
||||||
: never;
|
|
||||||
};
|
|
||||||
|
@ -17,24 +17,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { LazyComponent } from "@utils/misc";
|
import { LazyComponent } from "@utils/misc";
|
||||||
import { React } from "@webpack/common";
|
import { Margins, React } from "@webpack/common";
|
||||||
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
|
||||||
interface Props<T = any> {
|
interface Props {
|
||||||
/** Render nothing if an error occurs */
|
/** Render nothing if an error occurs */
|
||||||
noop?: boolean;
|
noop?: boolean;
|
||||||
/** Fallback component to render if an error occurs */
|
/** Fallback component to render if an error occurs */
|
||||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||||
/** called when an error occurs. The props property is only available if using .wrap */
|
/** called when an error occurs */
|
||||||
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
|
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||||
/** Custom error message */
|
/** Custom error message */
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
/** The props passed to the wrapped component. Only used by wrap */
|
|
||||||
wrappedProps?: T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = "#e78284";
|
const color = "#e78284";
|
||||||
@ -69,7 +65,7 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
|
this.props.onError?.(error, errorInfo);
|
||||||
logger.error("A component threw an Error\n", error);
|
logger.error("A component threw an Error\n", error);
|
||||||
logger.error("Component Stack", errorInfo.componentStack);
|
logger.error("Component Stack", errorInfo.componentStack);
|
||||||
}
|
}
|
||||||
@ -88,13 +84,15 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard style={{ overflow: "hidden" }}>
|
<ErrorCard style={{
|
||||||
|
overflow: "hidden",
|
||||||
|
}}>
|
||||||
<h1>Oh no!</h1>
|
<h1>Oh no!</h1>
|
||||||
<p>{msg}</p>
|
<p>{msg}</p>
|
||||||
<code>
|
<code>
|
||||||
{this.state.message}
|
{this.state.message}
|
||||||
{!!this.state.stack && (
|
{!!this.state.stack && (
|
||||||
<pre className={Margins.top8}>
|
<pre className={Margins.marginTop8}>
|
||||||
{this.state.stack}
|
{this.state.stack}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@ -105,11 +103,11 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
};
|
};
|
||||||
}) as
|
}) as
|
||||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
|
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||||
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
<ErrorBoundary {...errorBoundaryProps}>
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
.vc-error-card {
|
|
||||||
padding: 2em;
|
|
||||||
background-color: #e7828430;
|
|
||||||
border: 1px solid #e78284;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: var(--text-normal, white);
|
|
||||||
}
|
|
@ -16,15 +16,24 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./ErrorCard.css";
|
import { Card } from "@webpack/common";
|
||||||
|
|
||||||
import { classes } from "@utils/misc";
|
interface Props {
|
||||||
import type { HTMLProps } from "react";
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
|
}
|
||||||
|
export function ErrorCard(props: React.PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<div {...props} className={classes(props.className, "vc-error-card")}>
|
<Card className={props.className} style={
|
||||||
|
{
|
||||||
|
padding: "2em",
|
||||||
|
backgroundColor: "#e7828430",
|
||||||
|
borderColor: "#e78284",
|
||||||
|
color: "var(--text-normal)",
|
||||||
|
...props.style
|
||||||
|
}
|
||||||
|
}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { makeCodeblock } from "@utils/misc";
|
import { makeCodeblock } from "@utils/misc";
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
||||||
import { ReplaceFn } from "@utils/types";
|
|
||||||
import { search } from "@webpack";
|
import { search } from "@webpack";
|
||||||
import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { CheckedTextInput } from "./CheckedTextInput";
|
import { CheckedTextInput } from "./CheckedTextInput";
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
@ -130,7 +128,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!!diff?.length && (
|
{!!diff?.length && (
|
||||||
<Button className={Margins.top20} onClick={() => {
|
<Button className={Margins.marginTop20} onClick={() => {
|
||||||
try {
|
try {
|
||||||
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
||||||
setCompileResult([true, "Compiled successfully"]);
|
setCompileResult([true, "Compiled successfully"]);
|
||||||
@ -204,7 +202,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
className={Margins.top8}
|
className={Margins.marginTop8}
|
||||||
value={isFunc}
|
value={isFunc}
|
||||||
onChange={setIsFunc}
|
onChange={setIsFunc}
|
||||||
note="'replacement' will be evaled if this is toggled"
|
note="'replacement' will be evaled if this is toggled"
|
||||||
@ -258,7 +256,7 @@ function PatchHelper() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Text variant="heading-md/normal" tag="h2" className={Margins.bottom8}>Patch Helper</Text>
|
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
||||||
<Forms.FormTitle>find</Forms.FormTitle>
|
<Forms.FormTitle>find</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
@ -298,7 +296,7 @@ function PatchHelper() {
|
|||||||
|
|
||||||
{!!(find && match && replacement) && (
|
{!!(find && match && replacement) && (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
||||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||||
</>
|
</>
|
||||||
|
@ -38,12 +38,9 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
|
|||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
|
|
||||||
setError(null);
|
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
|
||||||
setState(`${Number.MAX_SAFE_INTEGER}`);
|
setState(`${Number.MAX_SAFE_INTEGER}`);
|
||||||
onChange(serialize(newValue));
|
onChange(serialize(newValue));
|
||||||
} else {
|
} else {
|
||||||
|
@ -36,7 +36,6 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
|
|||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
setError(null);
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
|||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
setError(null);
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
|
@ -30,12 +30,11 @@ import PluginModal from "@components/PluginSettings/PluginModal";
|
|||||||
import { Switch } from "@components/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
||||||
import { openModalLazy } from "@utils/modal";
|
import { openModalLazy } from "@utils/modal";
|
||||||
import { Plugin } from "@utils/types";
|
import { Plugin } from "@utils/types";
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
import { Alerts, Button, Card, Forms, Parser, React, Select, 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 Plugins from "~plugins";
|
||||||
|
|
||||||
@ -93,7 +92,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||||
const settings = useSettings([`plugins.${plugin.name}.enabled`]).plugins[plugin.name];
|
const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name];
|
||||||
|
|
||||||
const isEnabled = () => settings.enabled ?? false;
|
const isEnabled = () => settings.enabled ?? false;
|
||||||
|
|
||||||
@ -297,15 +296,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection className={Margins.top16}>
|
<Forms.FormSection className={Margins.marginTop16}>
|
||||||
<ReloadRequiredCard required={changes.hasChanges} />
|
<ReloadRequiredCard required={changes.hasChanges} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
Filters
|
Filters
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={cl("filter-controls")}>
|
<div className={cl("filter-controls")}>
|
||||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
|
||||||
<div className={InputStyles.inputWrapper}>
|
<div className={InputStyles.inputWrapper}>
|
||||||
<Select
|
<Select
|
||||||
className={InputStyles.inputDefault}
|
className={InputStyles.inputDefault}
|
||||||
@ -322,15 +321,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
{plugins}
|
{plugins}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.top20} />
|
<Forms.FormDivider className={Margins.marginTop20} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
Required Plugins
|
Required Plugins
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
import "./Switch.css";
|
import "./Switch.css";
|
||||||
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
|
||||||
interface SwitchProps {
|
interface SwitchProps {
|
||||||
@ -34,7 +33,7 @@ const SwitchClasses = findByPropsLazy("slider", "input", "container");
|
|||||||
export function Switch({ checked, onChange, disabled }: SwitchProps) {
|
export function Switch({ checked, onChange, disabled }: SwitchProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes(SwitchClasses.container, "default-colors", checked ? SwitchClasses.checked : void 0)} style={{
|
<div className={`${SwitchClasses.container} default-colors`} style={{
|
||||||
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
|
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
|
||||||
opacity: disabled ? 0.3 : 1
|
opacity: disabled ? 0.3 : 1
|
||||||
}}>
|
}}>
|
||||||
|
@ -18,26 +18,25 @@
|
|||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||||
import { Button, Card, Forms, Text } from "@webpack/common";
|
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
||||||
|
|
||||||
function BackupRestoreTab() {
|
function BackupRestoreTab() {
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
|
||||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||||
<Flex flexDirection="column">
|
<Flex flexDirection="column">
|
||||||
<strong>Warning</strong>
|
<strong>Warning</strong>
|
||||||
<span>Importing a settings file will overwrite your current settings.</span>
|
<span>Importing a settings file will overwrite your current settings.</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||||
You can import and export your Vencord settings as a JSON file.
|
You can import and export your Vencord settings as a JSON file.
|
||||||
This allows you to easily transfer your settings to another device,
|
This allows you to easily transfer your settings to another device,
|
||||||
or recover your settings after reinstalling Vencord or Discord.
|
or recover your settings after reinstalling Vencord or Discord.
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||||
Settings Export contains:
|
Settings Export contains:
|
||||||
<ul>
|
<ul>
|
||||||
<li>— Custom QuickCSS</li>
|
<li>— Custom QuickCSS</li>
|
||||||
|
@ -19,10 +19,9 @@
|
|||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
import { useAwaiter } from "@utils/misc";
|
||||||
import { findLazy } from "@webpack";
|
import { findLazy } from "@webpack";
|
||||||
import { Card, Forms, React, TextArea } from "@webpack/common";
|
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
|
||||||
|
|
||||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
@ -52,7 +51,7 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
|
||||||
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
||||||
<div>
|
<div>
|
||||||
{themeLinks.map(link => (
|
{themeLinks.map(link => (
|
||||||
@ -94,7 +93,7 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
||||||
<Forms.FormText>One link per line</Forms.FormText>
|
<Forms.FormText>One link per line</Forms.FormText>
|
||||||
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||||
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||||
<div style={{ marginBottom: ".5em" }}>
|
<div style={{ marginBottom: ".5em" }}>
|
||||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||||
@ -116,9 +115,13 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
</Card>
|
</Card>
|
||||||
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
style={{
|
||||||
|
padding: ".5em",
|
||||||
|
border: "1px solid var(--background-modifier-accent)"
|
||||||
|
}}
|
||||||
value={themeText}
|
value={themeText}
|
||||||
onChange={e => setThemeText(e.currentTarget.value)}
|
onChange={e => setThemeText(e.currentTarget.value)}
|
||||||
className={`${TextAreaProps.textarea} vc-settings-theme-links`}
|
className={TextAreaProps.textarea}
|
||||||
placeholder="Theme Links"
|
placeholder="Theme Links"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
@ -22,10 +22,9 @@ import { ErrorCard } from "@components/ErrorCard";
|
|||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes, useAwaiter } from "@utils/misc";
|
import { classes, useAwaiter } from "@utils/misc";
|
||||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||||
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
@ -110,14 +109,14 @@ function Updatable(props: CommonProps) {
|
|||||||
</ErrorCard>
|
</ErrorCard>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Forms.FormText className={Margins.bottom8}>
|
<Forms.FormText className={Margins.marginBottom8}>
|
||||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isOutdated && <Changes updates={updates} {...props} />}
|
{isOutdated && <Changes updates={updates} {...props} />}
|
||||||
|
|
||||||
<Flex className={classes(Margins.bottom8, Margins.top8)}>
|
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
|
||||||
{isOutdated && <Button
|
{isOutdated && <Button
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={isUpdating || isChecking}
|
disabled={isUpdating || isChecking}
|
||||||
@ -176,7 +175,7 @@ function Updatable(props: CommonProps) {
|
|||||||
function Newer(props: CommonProps) {
|
function Newer(props: CommonProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormText className={Margins.bottom8}>
|
<Forms.FormText className={Margins.marginBottom8}>
|
||||||
Your local copy has more recent commits. Please stash or reset them.
|
Your local copy has more recent commits. Please stash or reset them.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Changes {...props} updates={changes} />
|
<Changes {...props} updates={changes} />
|
||||||
@ -185,7 +184,7 @@ function Newer(props: CommonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Updater() {
|
function Updater() {
|
||||||
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
|
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]);
|
||||||
|
|
||||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||||
|
|
||||||
@ -200,12 +199,12 @@ function Updater() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection className={Margins.top16}>
|
<Forms.FormSection className={Margins.marginTop16}>
|
||||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.notifyAboutUpdates}
|
value={settings.notifyAboutUpdates}
|
||||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||||
note="Shows a notification on startup"
|
note="Shows a toast on startup"
|
||||||
disabled={settings.autoUpdate}
|
disabled={settings.autoUpdate}
|
||||||
>
|
>
|
||||||
Get notified about new updates
|
Get notified about new updates
|
||||||
@ -217,14 +216,6 @@ function Updater() {
|
|||||||
>
|
>
|
||||||
Automatically update
|
Automatically update
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch
|
|
||||||
value={settings.autoUpdateNotification}
|
|
||||||
onChange={(v: boolean) => settings.autoUpdateNotification = v}
|
|
||||||
note="Shows a notification when Vencord automatically updates"
|
|
||||||
disabled={!settings.autoUpdate}
|
|
||||||
>
|
|
||||||
Get notified when an automatic update completes
|
|
||||||
</Switch>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
||||||
|
|
||||||
@ -234,7 +225,7 @@ function Updater() {
|
|||||||
</Link>
|
</Link>
|
||||||
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
|
||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
@ -64,16 +63,12 @@ function VencordSettings() {
|
|||||||
title: "Enable React Developer Tools",
|
title: "Enable React Developer Tools",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
},
|
},
|
||||||
!IS_WEB && (!isWindows ? {
|
!IS_WEB && !isWindows && {
|
||||||
key: "frameless",
|
key: "frameless",
|
||||||
title: "Disable the window frame",
|
title: "Disable the window frame",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
} : {
|
},
|
||||||
key: "winNativeTitleBar",
|
!IS_WEB && {
|
||||||
title: "Use Windows' native title bar instead of Discord's custom one",
|
|
||||||
note: "Requires a full restart"
|
|
||||||
}),
|
|
||||||
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
|
|
||||||
key: "transparent",
|
key: "transparent",
|
||||||
title: "Enable window transparency",
|
title: "Enable window transparency",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
@ -166,7 +161,7 @@ function VencordSettings() {
|
|||||||
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
{ 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 Desktop notifications", value: "always" },
|
||||||
{ label: "Always use Vencord notifications", value: "never" },
|
{ label: "Always use Vencord notifications", value: "never" },
|
||||||
] satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
|
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
select={v => notifSettings.useNative = v}
|
select={v => notifSettings.useNative = v}
|
||||||
isSelected={v => v === notifSettings.useNative}
|
isSelected={v => v === notifSettings.useNative}
|
||||||
@ -180,7 +175,7 @@ function VencordSettings() {
|
|||||||
options={[
|
options={[
|
||||||
{ label: "Bottom Right", value: "bottom-right", default: true },
|
{ label: "Bottom Right", value: "bottom-right", default: true },
|
||||||
{ label: "Top Right", value: "top-right" },
|
{ label: "Top Right", value: "top-right" },
|
||||||
] satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
|
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
|
||||||
select={v => notifSettings.position = v}
|
select={v => notifSettings.position = v}
|
||||||
isSelected={v => v === notifSettings.position}
|
isSelected={v => v === notifSettings.position}
|
||||||
serialize={identity}
|
serialize={identity}
|
||||||
@ -199,29 +194,6 @@ function VencordSettings() {
|
|||||||
onMarkerRender={v => (v / 1000) + "s"}
|
onMarkerRender={v => (v / 1000) + "s"}
|
||||||
stickToMarkers={false}
|
stickToMarkers={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
|
|
||||||
<Forms.FormText className={Margins.bottom16}>
|
|
||||||
The amount of notifications to save in the log until old ones are removed.
|
|
||||||
Set to <code>0</code> to disable Notification log and <code>∞</code> to never automatically remove old Notifications
|
|
||||||
</Forms.FormText>
|
|
||||||
<Slider
|
|
||||||
markers={[0, 25, 50, 75, 100, 200]}
|
|
||||||
minValue={0}
|
|
||||||
maxValue={200}
|
|
||||||
stickToMarkers={true}
|
|
||||||
initialValue={notifSettings.logLimit}
|
|
||||||
onValueChange={v => notifSettings.logLimit = v}
|
|
||||||
onValueRender={v => v === 200 ? "∞" : v}
|
|
||||||
onMarkerRender={v => v === 200 ? "∞" : v}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={openNotificationLogModal}
|
|
||||||
disabled={notifSettings.logLimit === 0}
|
|
||||||
>
|
|
||||||
Open Notification Log
|
|
||||||
</Button>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ import "./settingsStyles.css";
|
|||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
|
||||||
import { findByCodeLazy } from "@webpack";
|
import { findByCodeLazy } from "@webpack";
|
||||||
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||||
|
|
||||||
@ -59,11 +58,11 @@ function Settings(props: SettingsProps) {
|
|||||||
const CurrentTab = SettingsTabs[tab]?.component;
|
const CurrentTab = SettingsTabs[tab]?.component;
|
||||||
|
|
||||||
return <Forms.FormSection>
|
return <Forms.FormSection>
|
||||||
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text>
|
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
||||||
|
|
||||||
<TabBar
|
<TabBar
|
||||||
type="top"
|
type={TabBar.Types.TOP}
|
||||||
look="brand"
|
look={TabBar.Looks.BRAND}
|
||||||
className={cl("tab-bar")}
|
className={cl("tab-bar")}
|
||||||
selectedItem={tab}
|
selectedItem={tab}
|
||||||
onItemSelect={SettingsRouter.open}
|
onItemSelect={SettingsRouter.open}
|
||||||
@ -84,7 +83,7 @@ function Settings(props: SettingsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function (props: SettingsProps) {
|
export default function (props: SettingsProps) {
|
||||||
return <ErrorBoundary onError={handleComponentFailed}>
|
return <ErrorBoundary>
|
||||||
<Settings tab={props.tab} />
|
<Settings tab={props.tab} />
|
||||||
</ErrorBoundary>;
|
</ErrorBoundary>;
|
||||||
}
|
}
|
||||||
|
@ -38,11 +38,3 @@
|
|||||||
color: var(--info-warning-text);
|
color: var(--info-warning-text);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-settings-theme-links {
|
|
||||||
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
|
|
||||||
display: inline-block !important;
|
|
||||||
color: var(--text-normal) !important;
|
|
||||||
padding: 0.5em;
|
|
||||||
border: 1px solid var(--background-modifier-accent);
|
|
||||||
}
|
|
||||||
|
@ -16,12 +16,29 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
import { isOutdated, rebuild, update } from "@utils/updater";
|
||||||
|
|
||||||
export function handleComponentFailed() {
|
export async function handleComponentFailed() {
|
||||||
maybePromptToUpdate(
|
if (isOutdated) {
|
||||||
"Uh Oh! Failed to render this Page." +
|
setImmediate(async () => {
|
||||||
" However, there is an update available that might fix it." +
|
const wantsUpdate = confirm(
|
||||||
" Would you like to update and restart now?"
|
"Uh Oh! Failed to render this Page." +
|
||||||
);
|
" However, there is an update available that might fix it." +
|
||||||
|
" Would you like to update and restart now?"
|
||||||
|
);
|
||||||
|
if (wantsUpdate) {
|
||||||
|
try {
|
||||||
|
await update();
|
||||||
|
await rebuild();
|
||||||
|
if (IS_WEB)
|
||||||
|
location.reload();
|
||||||
|
else
|
||||||
|
DiscordNative.app.relaunch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("That also failed :( Try updating or reinstalling with the installer!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
@ -51,7 +51,8 @@ declare global {
|
|||||||
* Only available when running in Electron, undefined on web.
|
* Only available when running in Electron, undefined on web.
|
||||||
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
||||||
*
|
*
|
||||||
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
|
* If you really must use it, mark your plugin as Desktop App only via
|
||||||
|
* `target: "DESKTOP"`
|
||||||
*/
|
*/
|
||||||
export var DiscordNative: any;
|
export var DiscordNative: any;
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { onceDefined } from "@utils/onceDefined";
|
import { onceDefined } from "@utils/onceDefined";
|
||||||
import electron, { app, BrowserWindowConstructorOptions, Menu, protocol, session } from "electron";
|
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
|
|
||||||
import { initIpc } from "./ipcMain";
|
import { initIpc } from "./ipcMain";
|
||||||
@ -79,12 +79,8 @@ if (!process.argv.includes("--vanilla")) {
|
|||||||
options.webPreferences.sandbox = false;
|
options.webPreferences.sandbox = false;
|
||||||
if (settings.frameless) {
|
if (settings.frameless) {
|
||||||
options.frame = false;
|
options.frame = false;
|
||||||
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
|
||||||
delete options.frame;
|
|
||||||
}
|
}
|
||||||
|
if (settings.transparent) {
|
||||||
// This causes electron to freeze / white screen for some people
|
|
||||||
if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
|
|
||||||
options.transparent = true;
|
options.transparent = true;
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
}
|
}
|
||||||
@ -117,10 +113,10 @@ if (!process.argv.includes("--vanilla")) {
|
|||||||
|
|
||||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
electron.app.whenReady().then(() => {
|
||||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||||
// from a string I don't think any other form of sourcemaps would work
|
// from a string I don't think any other form of sourcemaps would work
|
||||||
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
||||||
let url = unsafeUrl.slice("vencord://".length);
|
let url = unsafeUrl.slice("vencord://".length);
|
||||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||||
switch (url) {
|
switch (url) {
|
||||||
@ -176,7 +172,7 @@ if (!process.argv.includes("--vanilla")) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
||||||
if (responseHeaders) {
|
if (responseHeaders) {
|
||||||
if (resourceType === "mainFrame")
|
if (resourceType === "mainFrame")
|
||||||
patchCsp(responseHeaders, "content-security-policy");
|
patchCsp(responseHeaders, "content-security-policy");
|
||||||
@ -188,11 +184,6 @@ if (!process.argv.includes("--vanilla")) {
|
|||||||
}
|
}
|
||||||
cb({ cancel: false, responseHeaders });
|
cb({ cancel: false, responseHeaders });
|
||||||
});
|
});
|
||||||
|
|
||||||
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
|
|
||||||
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
|
|
||||||
// impossible to load css from github raw despite our fix above
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived = () => { };
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||||
|
@ -32,10 +32,10 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '"7z","ade","adp"',
|
find: "\"github.com\":new RegExp(\"\\\\/releases\\\\S*\\\\/download\"),",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /JSON\.parse\('\[.+?'\)/,
|
match: /const o=JSON.parse\('\[.+?'\)/,
|
||||||
replace: "[]"
|
replace: "const o=[]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -24,10 +24,9 @@ import { Heart } from "@components/Heart";
|
|||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Forms } from "@webpack/common";
|
import { Forms, Margins } from "@webpack/common";
|
||||||
|
|
||||||
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
||||||
|
|
||||||
@ -151,7 +150,7 @@ export default definePlugin({
|
|||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
This Badge is a special perk for Vencord Donors
|
This Badge is a special perk for Vencord Donors
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Forms.FormText className={Margins.top20}>
|
<Forms.FormText className={Margins.marginTop20}>
|
||||||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,99 +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 { Devs } from "@utils/constants";
|
|
||||||
import definePlugin, { type PatchReplacement } from "@utils/types";
|
|
||||||
import { addListener, removeListener } from "@webpack";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last var name corresponding to the Context Menu API (Discord, not ours) module
|
|
||||||
*/
|
|
||||||
let lastVarName = "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param target The patch replacement object
|
|
||||||
* @param exportKey The key exporting the build Context Menu component function
|
|
||||||
*/
|
|
||||||
function makeReplacementProxy(target: PatchReplacement, exportKey: string) {
|
|
||||||
return new Proxy(target, {
|
|
||||||
get(_, p) {
|
|
||||||
if (p === "match") return RegExp(`${exportKey},{(?<=${lastVarName}\\.${exportKey},{)`, "g");
|
|
||||||
// @ts-expect-error
|
|
||||||
return Reflect.get(...arguments);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function listener(exports: any, id: number) {
|
|
||||||
if (!Settings.plugins.ContextMenuAPI.enabled) return removeListener(listener);
|
|
||||||
|
|
||||||
if (typeof exports !== "object" || exports === null) return;
|
|
||||||
|
|
||||||
for (const key in exports) if (key.length <= 3) {
|
|
||||||
const prop = exports[key];
|
|
||||||
if (typeof prop !== "function") continue;
|
|
||||||
|
|
||||||
const str = Function.prototype.toString.call(prop);
|
|
||||||
if (str.includes('path:["empty"]')) {
|
|
||||||
Vencord.Plugins.patches.push({
|
|
||||||
plugin: "ContextMenuAPI",
|
|
||||||
all: true,
|
|
||||||
noWarn: true,
|
|
||||||
find: "navId:",
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
// Set the lastVarName for our proxy to use
|
|
||||||
match: RegExp(`${id}(?<=(\\i)=.+?)`),
|
|
||||||
replace: (id, varName) => {
|
|
||||||
lastVarName = varName;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* We are using a proxy here to utilize the whole code the patcher gives us, instead of matching the entire module (which is super slow)
|
|
||||||
* Our proxy returns the corresponding match for that module utilizing lastVarName, which is set by the patch before
|
|
||||||
*/
|
|
||||||
makeReplacementProxy({
|
|
||||||
match: "", // Needed to canonicalizeDescriptor
|
|
||||||
replace: "$&contextMenuApiArguments:arguments,",
|
|
||||||
}, key)
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
removeListener(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener(listener);
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "ContextMenuAPI",
|
|
||||||
description: "API for adding/removing items to/from context menus.",
|
|
||||||
authors: [Devs.Nuckyz],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "♫ (つ。◕‿‿◕。)つ ♪",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=function \i\((\i)\){)(?=var \i,\i=\i\.navId)/,
|
|
||||||
replace: (_, props) => `Vencord.Api.ContextMenu._patchContextMenu(${props});`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
@ -43,7 +43,7 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: '"Menu API',
|
find: '"Menu API',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s,
|
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s,
|
||||||
replace: (m, mod) => {
|
replace: (m, mod) => {
|
||||||
let nicenNames = "";
|
let nicenNames = "";
|
||||||
const redefines = [] as string[];
|
const redefines = [] as string[];
|
||||||
|
@ -22,22 +22,22 @@ import definePlugin from "@utils/types";
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageEventsAPI",
|
name: "MessageEventsAPI",
|
||||||
description: "Api required by anything using message events.",
|
description: "Api required by anything using message events.",
|
||||||
authors: [Devs.Arjix, Devs.hunt],
|
authors: [Devs.Arjix],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '"MessageActionCreators"',
|
find: "sendMessage:function",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /_sendMessage:(function\([^)]+\)){/,
|
match: /(?<=_sendMessage:function\([^)]+\)){/,
|
||||||
replace: "_sendMessage:async $1{if(await Vencord.Api.MessageEvents._handlePreSend(...arguments))return;"
|
replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};"
|
||||||
}, {
|
}, {
|
||||||
match: /\beditMessage:(function\([^)]+\)){/,
|
match: /(?<=\beditMessage:function\([^)]+\)){/,
|
||||||
replace: "editMessage:async $1{await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '("interactionUsernameProfile',
|
find: '("interactionUsernameProfile',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /var \i=(\i)\.id,\i=(\i)\.id;return \i\.useCallback\(\(?function\((\i)\){/,
|
match: /var \w=(\w)\.id,\w=(\w)\.id;return .{1,2}\.useCallback\(\(?function\((.{1,2})\){/,
|
||||||
replace: (m, message, channel, event) =>
|
replace: (m, message, channel, event) =>
|
||||||
// the message param is shadowed by the event param, so need to alias them
|
// the message param is shadowed by the event param, so need to alias them
|
||||||
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`
|
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`
|
||||||
|
@ -22,16 +22,16 @@ import definePlugin from "@utils/types";
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessagePopoverAPI",
|
name: "MessagePopoverAPI",
|
||||||
description: "API to add buttons to message popovers.",
|
description: "API to add buttons to message popovers.",
|
||||||
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
|
authors: [Devs.KingFish, Devs.Ven],
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
||||||
replacement: {
|
replacement: {
|
||||||
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
|
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
|
||||||
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
|
match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
|
||||||
replace: (m, makeElement) => {
|
replace: (m, bools, makeElement) => {
|
||||||
const msg = m.match(/message:(.{1,3}),/)?.[1];
|
const msg = m.match(/message:(.{1,3}),/)?.[1];
|
||||||
if (!msg) throw new Error("Could not find message variable");
|
if (!msg) throw new Error("Could not find message variable");
|
||||||
return `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`;
|
return `...(${bools}?Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}):[]),${m}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
@ -29,12 +29,13 @@ export default definePlugin({
|
|||||||
find: 'displayName="NoticeStore"',
|
find: 'displayName="NoticeStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?=;\i=null;.{0,70}getPremiumSubscription)/g,
|
match: /;.{1,2}=null;.{0,70}getPremiumSubscription/g,
|
||||||
replace: ";if(Vencord.Api.Notices.currentNotice)return false"
|
replace:
|
||||||
|
";if(Vencord.Api.Notices.currentNotice)return false$&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
|
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
|
||||||
replace: (_, notice) => `if(${notice}.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);`
|
replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ export default definePlugin({
|
|||||||
name: "WebRichPresence (arRPC)",
|
name: "WebRichPresence (arRPC)",
|
||||||
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
||||||
authors: [Devs.Ducko],
|
authors: [Devs.Ducko],
|
||||||
|
target: "WEB",
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
@ -59,9 +60,6 @@ export default definePlugin({
|
|||||||
),
|
),
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users
|
|
||||||
if ("armcord" in window) return;
|
|
||||||
|
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
|
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
|
||||||
|
|
@ -37,11 +37,11 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '"dot"===',
|
find: '"username"===',
|
||||||
all: true,
|
all: true,
|
||||||
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
|
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /"(?:username|dot)"===\i(?!\.\i)/g,
|
match: /"(?:username|dot)"===\w(?!\.\w)/g,
|
||||||
replace: "true",
|
replace: "true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -27,16 +27,11 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: "Masks.STATUS_ONLINE",
|
find: "Masks.STATUS_ONLINE",
|
||||||
replacement: {
|
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,
|
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
|
||||||
replace: "Masks.STATUS_ONLINE"
|
replace: "Masks.STATUS_ONLINE"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".AVATAR_STATUS_MOBILE_16;",
|
|
||||||
replacement: {
|
|
||||||
match: /(\.fromIsMobile,.+?)\i.status/,
|
|
||||||
replace: (_, rest) => `${rest}"online"`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -18,9 +18,6 @@
|
|||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import * as Webpack from "@webpack";
|
|
||||||
import { extract, filters, findAll, search } from "@webpack";
|
|
||||||
import { React } from "@webpack/common";
|
|
||||||
|
|
||||||
const WEB_ONLY = (f: string) => () => {
|
const WEB_ONLY = (f: string) => () => {
|
||||||
throw new Error(`'${f}' is Discord Desktop only.`);
|
throw new Error(`'${f}' is Discord Desktop only.`);
|
||||||
@ -32,48 +29,19 @@ export default definePlugin({
|
|||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
getShortcuts() {
|
getShortcuts() {
|
||||||
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
|
|
||||||
const cache = new Map<string, unknown>();
|
|
||||||
|
|
||||||
return function (...filterProps: unknown[]) {
|
|
||||||
const cacheKey = String(filterProps);
|
|
||||||
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
||||||
|
|
||||||
const matches = findAll(filterFactory(...filterProps));
|
|
||||||
|
|
||||||
const result = (() => {
|
|
||||||
switch (matches.length) {
|
|
||||||
case 0: return null;
|
|
||||||
case 1: return matches[0];
|
|
||||||
default:
|
|
||||||
const uniqueMatches = [...new Set(matches)];
|
|
||||||
if (uniqueMatches.length > 1)
|
|
||||||
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
|
|
||||||
|
|
||||||
return matches[0];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
if (result && cacheKey) cache.set(cacheKey, result);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy,
|
||||||
|
fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read,
|
||||||
wp: Vencord.Webpack,
|
wp: Vencord.Webpack,
|
||||||
wpc: Webpack.wreq.c,
|
wpc: Vencord.Webpack.wreq.c,
|
||||||
wreq: Webpack.wreq,
|
wreq: Vencord.Webpack.wreq,
|
||||||
wpsearch: search,
|
wpsearch: Vencord.Webpack.search,
|
||||||
wpex: extract,
|
wpex: Vencord.Webpack.extract,
|
||||||
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
||||||
find: newFindWrapper(f => f),
|
findByProps: Vencord.Webpack.findByProps,
|
||||||
findAll,
|
find: Vencord.Webpack.find,
|
||||||
findByProps: newFindWrapper(filters.byProps),
|
Plugins: Vencord.Plugins,
|
||||||
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
|
React: Vencord.Webpack.Common.React,
|
||||||
findByCode: newFindWrapper(filters.byCode),
|
|
||||||
findAllByCode: (code: string) => findAll(filters.byCode(code)),
|
|
||||||
PluginsApi: Vencord.Plugins,
|
|
||||||
plugins: Vencord.Plugins.plugins,
|
|
||||||
React,
|
|
||||||
Settings: Vencord.Settings,
|
Settings: Vencord.Settings,
|
||||||
Api: Vencord.Api,
|
Api: Vencord.Api,
|
||||||
reload: () => location.reload(),
|
reload: () => location.reload(),
|
||||||
|
@ -1,159 +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 { showNotification } from "@api/Notifications";
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { closeAllModals } from "@utils/modal";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
|
||||||
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
|
||||||
import type { ReactElement } from "react";
|
|
||||||
|
|
||||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
attemptToPreventCrashes: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Whether to attempt to prevent Discord crashes.",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
attemptToNavigateToHome: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Whether to attempt to navigate to the home when preventing Discord crashes.",
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let crashCount: number = 0;
|
|
||||||
let lastCrashTimestamp: number = 0;
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "CrashHandler",
|
|
||||||
description: "Utility plugin for handling and possibly recovering from Crashes without a restart",
|
|
||||||
authors: [Devs.Nuckyz],
|
|
||||||
enabledByDefault: true,
|
|
||||||
|
|
||||||
popAllModals: undefined as (() => void) | undefined,
|
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: ".Messages.ERRORS_UNEXPECTED_CRASH",
|
|
||||||
replacement: {
|
|
||||||
match: /(?=this\.setState\()/,
|
|
||||||
replace: "$self.handleCrash(this)||"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: 'dispatch({type:"MODAL_POP_ALL"})',
|
|
||||||
replacement: {
|
|
||||||
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
|
|
||||||
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
|
||||||
if (++crashCount > 5) {
|
|
||||||
try {
|
|
||||||
showNotification({
|
|
||||||
color: "#eed202",
|
|
||||||
title: "Discord has crashed!",
|
|
||||||
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
lastCrashTimestamp = Date.now();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => crashCount--, 60_000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
|
|
||||||
|
|
||||||
if (settings.store.attemptToPreventCrashes) {
|
|
||||||
this.handlePreventCrash(_this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.error("Failed to handle crash", err);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
lastCrashTimestamp = Date.now();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
|
||||||
if (Date.now() - lastCrashTimestamp >= 1_000) {
|
|
||||||
try {
|
|
||||||
showNotification({
|
|
||||||
color: "#eed202",
|
|
||||||
title: "Discord has crashed!",
|
|
||||||
body: "Attempting to recover...",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.popAllModals?.();
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
closeAllModals();
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to close all open modals.", err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" });
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to close user popout.", err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to pop all layers.", err);
|
|
||||||
}
|
|
||||||
if (settings.store.attemptToNavigateToHome) {
|
|
||||||
try {
|
|
||||||
NavigationRouter.transitionTo("/channels/@me");
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to navigate to home", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
_this.forceUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -19,7 +19,6 @@
|
|||||||
import { definePluginSettings } from "@api/settings";
|
import { definePluginSettings } from "@api/settings";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isTruthy } from "@utils/guards";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
import { useAwaiter } from "@utils/misc";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
||||||
@ -57,11 +56,11 @@ interface ActivityAssets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Activity {
|
interface Activity {
|
||||||
state?: string;
|
state: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
timestamps?: {
|
timestamps?: {
|
||||||
start?: number;
|
start?: Number;
|
||||||
end?: number;
|
end?: Number;
|
||||||
};
|
};
|
||||||
assets?: ActivityAssets;
|
assets?: ActivityAssets;
|
||||||
buttons?: Array<string>;
|
buttons?: Array<string>;
|
||||||
@ -71,7 +70,7 @@ interface Activity {
|
|||||||
button_urls?: Array<string>;
|
button_urls?: Array<string>;
|
||||||
};
|
};
|
||||||
type: ActivityType;
|
type: ActivityType;
|
||||||
flags: number;
|
flags: Number;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ActivityType {
|
enum ActivityType {
|
||||||
@ -94,13 +93,13 @@ const numOpt = (description: string) => ({
|
|||||||
onChange: setRpc
|
onChange: setRpc
|
||||||
}) as const;
|
}) as const;
|
||||||
|
|
||||||
const choice = (label: string, value: any, _default?: boolean) => ({
|
const choice = (label: string, value: any, _default?: Boolean) => ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
default: _default
|
default: _default
|
||||||
}) as const;
|
}) as const;
|
||||||
|
|
||||||
const choiceOpt = <T,>(description: string, options: T) => ({
|
const choiceOpt = (description: string, options) => ({
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description,
|
description,
|
||||||
onChange: setRpc,
|
onChange: setRpc,
|
||||||
@ -174,13 +173,13 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||||||
activity.buttons = [
|
activity.buttons = [
|
||||||
buttonOneText,
|
buttonOneText,
|
||||||
buttonTwoText
|
buttonTwoText
|
||||||
].filter(isTruthy);
|
].filter(Boolean);
|
||||||
|
|
||||||
activity.metadata = {
|
activity.metadata = {
|
||||||
button_urls: [
|
button_urls: [
|
||||||
buttonOneURL,
|
buttonOneURL,
|
||||||
buttonTwoURL
|
buttonTwoURL
|
||||||
].filter(isTruthy)
|
].filter(Boolean)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,16 +206,17 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||||||
delete activity[k];
|
delete activity[k];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WHAT DO YOU WANT FROM ME
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setRpc(disable?: boolean) {
|
async function setRpc(disable?: Boolean) {
|
||||||
const activity: Activity | undefined = await createActivity();
|
const activity: Activity | undefined = await createActivity();
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "LOCAL_ACTIVITY_UPDATE",
|
type: "LOCAL_ACTIVITY_UPDATE",
|
||||||
activity: !disable ? activity : null,
|
activity: !disable ? activity : {}
|
||||||
socketId: "CustomRPC",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,268 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
|
||||||
import { showNotification } from "@api/Notifications";
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { filters, findAll, search } from "@webpack";
|
|
||||||
import { Menu } from "@webpack/common";
|
|
||||||
|
|
||||||
const PORT = 8485;
|
|
||||||
const NAV_ID = "dev-companion-reconnect";
|
|
||||||
|
|
||||||
const logger = new Logger("DevCompanion");
|
|
||||||
|
|
||||||
let socket: WebSocket | undefined;
|
|
||||||
|
|
||||||
type Node = StringNode | RegexNode | FunctionNode;
|
|
||||||
|
|
||||||
interface StringNode {
|
|
||||||
type: "string";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegexNode {
|
|
||||||
type: "regex";
|
|
||||||
value: {
|
|
||||||
pattern: string;
|
|
||||||
flags: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FunctionNode {
|
|
||||||
type: "function";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PatchData {
|
|
||||||
find: string;
|
|
||||||
replacement: {
|
|
||||||
match: StringNode | RegexNode;
|
|
||||||
replace: StringNode | FunctionNode;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FindData {
|
|
||||||
type: string;
|
|
||||||
args: Array<StringNode | FunctionNode>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
notifyOnAutoConnect: {
|
|
||||||
description: "Whether to notify when Dev Companion has automatically connected.",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function parseNode(node: Node) {
|
|
||||||
switch (node.type) {
|
|
||||||
case "string":
|
|
||||||
return node.value;
|
|
||||||
case "regex":
|
|
||||||
return new RegExp(node.value.pattern, node.value.flags);
|
|
||||||
case "function":
|
|
||||||
// We LOVE remote code execution
|
|
||||||
// Safety: This comes from localhost only, which actually means we have less permissions than the source,
|
|
||||||
// since we're running in the browser sandbox, whereas the sender has host access
|
|
||||||
return (0, eval)(node.value);
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown Node Type " + (node as any).type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initWs(isManual = false) {
|
|
||||||
let wasConnected = isManual;
|
|
||||||
let hasErrored = false;
|
|
||||||
const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
|
|
||||||
|
|
||||||
ws.addEventListener("open", () => {
|
|
||||||
wasConnected = true;
|
|
||||||
|
|
||||||
logger.info("Connected to WebSocket");
|
|
||||||
|
|
||||||
(settings.store.notifyOnAutoConnect || isManual) && showNotification({
|
|
||||||
title: "Dev Companion Connected",
|
|
||||||
body: "Connected to WebSocket"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener("error", e => {
|
|
||||||
if (!wasConnected) return;
|
|
||||||
|
|
||||||
hasErrored = true;
|
|
||||||
|
|
||||||
logger.error("Dev Companion Error:", e);
|
|
||||||
|
|
||||||
showNotification({
|
|
||||||
title: "Dev Companion Error",
|
|
||||||
body: (e as ErrorEvent).message || "No Error Message",
|
|
||||||
color: "var(--status-danger, red)",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener("close", e => {
|
|
||||||
if (!wasConnected || hasErrored) return;
|
|
||||||
|
|
||||||
logger.info("Dev Companion Disconnected:", e.code, e.reason);
|
|
||||||
|
|
||||||
showNotification({
|
|
||||||
title: "Dev Companion Disconnected",
|
|
||||||
body: e.reason || "No Reason provided",
|
|
||||||
color: "var(--status-danger, red)",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener("message", e => {
|
|
||||||
try {
|
|
||||||
var { nonce, type, data } = JSON.parse(e.data);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Invalid JSON:", err, "\n" + e.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function reply(error?: string) {
|
|
||||||
const data = { nonce, ok: !error } as Record<string, unknown>;
|
|
||||||
if (error) data.error = error;
|
|
||||||
|
|
||||||
ws.send(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Received Message:", type, "\n", data);
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "testPatch": {
|
|
||||||
const { find, replacement } = data as PatchData;
|
|
||||||
|
|
||||||
const candidates = search(find);
|
|
||||||
const keys = Object.keys(candidates);
|
|
||||||
if (keys.length !== 1)
|
|
||||||
return reply("Expected exactly one 'find' matches, found " + keys.length);
|
|
||||||
|
|
||||||
const mod = candidates[keys[0]];
|
|
||||||
let src = String(mod.original ?? mod);
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
for (const { match, replace } of replacement) {
|
|
||||||
i++;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const matcher = canonicalizeMatch(parseNode(match));
|
|
||||||
const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName");
|
|
||||||
|
|
||||||
const newSource = src.replace(matcher, replacement as string);
|
|
||||||
|
|
||||||
if (src === newSource) throw "Had no effect";
|
|
||||||
Function(newSource);
|
|
||||||
|
|
||||||
src = newSource;
|
|
||||||
} catch (err) {
|
|
||||||
return reply(`Replacement ${i} failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reply();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "testFind": {
|
|
||||||
const { type, args } = data as FindData;
|
|
||||||
try {
|
|
||||||
var parsedArgs = args.map(parseNode);
|
|
||||||
} catch (err) {
|
|
||||||
return reply("Failed to parse args: " + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let results: any[];
|
|
||||||
switch (type.replace("find", "").replace("Lazy", "")) {
|
|
||||||
case "":
|
|
||||||
results = findAll(parsedArgs[0]);
|
|
||||||
break;
|
|
||||||
case "ByProps":
|
|
||||||
results = findAll(filters.byProps(...parsedArgs));
|
|
||||||
break;
|
|
||||||
case "Store":
|
|
||||||
results = findAll(filters.byStoreName(parsedArgs[0]));
|
|
||||||
break;
|
|
||||||
case "ByCode":
|
|
||||||
results = findAll(filters.byCode(...parsedArgs));
|
|
||||||
break;
|
|
||||||
case "ModuleId":
|
|
||||||
results = Object.keys(search(parsedArgs[0]));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return reply("Unknown Find Type " + type);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueResultsCount = new Set(results).size;
|
|
||||||
if (uniqueResultsCount === 0) throw "No results";
|
|
||||||
if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific";
|
|
||||||
} catch (err) {
|
|
||||||
return reply("Failed to find: " + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
reply();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
reply("Unknown Type " + type);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextMenuPatch: NavContextMenuPatchCallback = kids => {
|
|
||||||
if (kids.some(k => k?.props?.id === NAV_ID)) return;
|
|
||||||
|
|
||||||
kids.unshift(
|
|
||||||
<Menu.MenuItem
|
|
||||||
id={NAV_ID}
|
|
||||||
label="Reconnect Dev Companion"
|
|
||||||
action={() => {
|
|
||||||
socket?.close(1000, "Reconnecting");
|
|
||||||
initWs(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "DevCompanion",
|
|
||||||
description: "Dev Companion Plugin",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
dependencies: ["ContextMenuAPI"],
|
|
||||||
settings,
|
|
||||||
|
|
||||||
start() {
|
|
||||||
initWs();
|
|
||||||
addContextMenuPatch("user-settings-cog", contextMenuPatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
socket?.close(1000, "Plugin Stopped");
|
|
||||||
socket = void 0;
|
|
||||||
removeContextMenuPatch("user-settings-cog", contextMenuPatch);
|
|
||||||
}
|
|
||||||
});
|
|
@ -27,9 +27,9 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=function \i\(\){)(?=.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/,
|
match: /function (?<functionName>.{1,3})\(\){.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT.+?}}/,
|
||||||
replace: "return;"
|
replace: "function $<functionName>(){}",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
@ -16,15 +16,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { migratePluginSettings, Settings } from "@api/settings";
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { makeLazy } from "@utils/misc";
|
||||||
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
import { Forms, GuildStore, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
import { Forms, GuildStore, Margins, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.top20}>Custom Name</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20}>Custom Name</Forms.FormTitle>
|
||||||
<CheckedTextInput
|
<CheckedTextInput
|
||||||
value={name}
|
value={name}
|
||||||
onChange={setName}
|
onChange={setName}
|
||||||
@ -175,12 +175,50 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMenuItem(id: string, name: string, isAnimated: boolean) {
|
migratePluginSettings("EmoteCloner", "EmoteYoink");
|
||||||
return (
|
export default definePlugin({
|
||||||
<Menu.MenuItem
|
name: "EmoteCloner",
|
||||||
|
description: "Adds a Clone context menu item to emotes to clone them your own server",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
dependencies: ["MenuItemDeobfuscatorAPI"],
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
// Literally copy pasted from ReverseImageSearch lol
|
||||||
|
find: "open-native-link",
|
||||||
|
replacement: {
|
||||||
|
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
||||||
|
replace: "$&,$self.makeMenu(arguments[2])"
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
// Also copy pasted from Reverse Image Search
|
||||||
|
{
|
||||||
|
// pass the target to the open link menu so we can grab its data
|
||||||
|
find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,",
|
||||||
|
predicate: makeLazy(() => !Settings.plugins.ReverseImageSearch.enabled),
|
||||||
|
noWarn: true,
|
||||||
|
replacement: {
|
||||||
|
match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/,
|
||||||
|
replace: "$&,$<props>.target"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
|
makeMenu(htmlElement: HTMLImageElement) {
|
||||||
|
if (htmlElement?.dataset.type !== "emoji")
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const { id } = htmlElement.dataset;
|
||||||
|
const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1];
|
||||||
|
|
||||||
|
if (!name || !id)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif");
|
||||||
|
|
||||||
|
return <Menu.MenuItem
|
||||||
id="emote-cloner"
|
id="emote-cloner"
|
||||||
key="emote-cloner"
|
key="emote-cloner"
|
||||||
label="Clone Emote"
|
label="Clone"
|
||||||
action={() =>
|
action={() =>
|
||||||
openModal(modalProps => (
|
openModal(modalProps => (
|
||||||
<ModalRoot {...modalProps}>
|
<ModalRoot {...modalProps}>
|
||||||
@ -202,51 +240,7 @@ function buildMenuItem(id: string, name: string, isAnimated: boolean) {
|
|||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
);
|
</Menu.MenuItem>;
|
||||||
}
|
|
||||||
|
|
||||||
function isGifUrl(url: string) {
|
|
||||||
return new URL(url).pathname.endsWith(".gif");
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
|
||||||
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
|
|
||||||
|
|
||||||
if (!favoriteableId || favoriteableType !== "emoji") return;
|
|
||||||
|
|
||||||
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
|
|
||||||
if (!match) return;
|
|
||||||
const name = match[1] ?? "FakeNitroEmoji";
|
|
||||||
|
|
||||||
const group = findGroupChildrenByChildId("copy-link", children);
|
|
||||||
if (group && !group.some(child => child?.props?.id === "emote-cloner"))
|
|
||||||
group.push(buildMenuItem(favoriteableId, name, isGifUrl(itemHref ?? itemSrc)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
|
|
||||||
const { id, name, type } = props?.target?.dataset ?? {};
|
|
||||||
if (!id || !name || type !== "emoji") return;
|
|
||||||
|
|
||||||
const firstChild = props.target.firstChild as HTMLImageElement;
|
|
||||||
|
|
||||||
if (!children.some(c => c?.props?.id === "emote-cloner"))
|
|
||||||
children.push(buildMenuItem(id, name, firstChild && isGifUrl(firstChild.src)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "EmoteCloner",
|
|
||||||
description: "Adds a Clone context menu item to emotes to clone them your own server",
|
|
||||||
authors: [Devs.Ven, Devs.Nuckyz],
|
|
||||||
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
|
|
||||||
|
|
||||||
start() {
|
|
||||||
addContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
addContextMenuPatch("expression-picker", expressionPickerPatch);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
removeContextMenuPatch("expression-picker", expressionPickerPatch);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
@ -24,71 +24,49 @@ import { Forms, React } from "@webpack/common";
|
|||||||
|
|
||||||
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
enableIsStaff: {
|
|
||||||
description: "Enable isStaff",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
forceStagingBanner: {
|
|
||||||
description: "Whether to force Staging banner under user area.",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false,
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Experiments",
|
name: "Experiments",
|
||||||
description: "Enable Access to Experiments in Discord!",
|
|
||||||
authors: [
|
authors: [
|
||||||
Devs.Megu,
|
Devs.Megu,
|
||||||
Devs.Ven,
|
Devs.Ven,
|
||||||
Devs.Nickyux,
|
Devs.Nickyux,
|
||||||
Devs.BanTheNons,
|
Devs.BanTheNons
|
||||||
Devs.Nuckyz
|
|
||||||
],
|
],
|
||||||
settings,
|
description: "Enable Access to Experiments in Discord!",
|
||||||
|
patches: [{
|
||||||
patches: [
|
find: "Object.defineProperties(this,{isDeveloper",
|
||||||
{
|
replacement: {
|
||||||
find: "Object.defineProperties(this,{isDeveloper",
|
match: /(?<={isDeveloper:\{[^}]+,get:function\(\)\{return )\w/,
|
||||||
replacement: {
|
replace: "true"
|
||||||
match: /(?<={isDeveloper:\{[^}]+?,get:function\(\)\{return )\w/,
|
|
||||||
replace: "true"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
}, {
|
||||||
find: 'type:"user",revision',
|
find: 'type:"user",revision',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /!(\i)&&"CONNECTION_OPEN".+?;/g,
|
match: /!(\w{1,3})&&"CONNECTION_OPEN".+?;/g,
|
||||||
replace: "$1=!0;"
|
replace: "$1=!0;"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
}, {
|
||||||
find: ".isStaff=function(){",
|
find: ".isStaff=function(){",
|
||||||
predicate: () => settings.store.enableIsStaff,
|
predicate: () => Settings.plugins.Experiments.enableIsStaff === true,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /return\s*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/,
|
match: /return\s*(\w+)\.hasFlag\((.+?)\.STAFF\)}/,
|
||||||
replace: (_, user, flags) => `return Vencord.Webpack.Common.UserStore.getCurrentUser().id===${user}.id||${user}.hasFlag(${flags}.STAFF)}`
|
replace: "return Vencord.Webpack.Common.UserStore.getCurrentUser().id===$1.id||$1.hasFlag($2.STAFF)}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*?\|\|/,
|
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*\|\|/,
|
||||||
replace: "hasFreePremium=function(){return ",
|
replace: "hasFreePremium=function(){return ",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
}],
|
||||||
{
|
options: {
|
||||||
find: ".Messages.DEV_NOTICE_STAGING",
|
enableIsStaff: {
|
||||||
predicate: () => settings.store.forceStagingBanner,
|
description: "Enable isStaff (requires restart)",
|
||||||
replacement: {
|
type: OptionType.BOOLEAN,
|
||||||
match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/,
|
default: false,
|
||||||
replace: "true"
|
restartNeeded: true,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
settingsAboutComponent: () => {
|
||||||
const isMacOS = navigator.platform.includes("Mac");
|
const isMacOS = navigator.platform.includes("Mac");
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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: "F8Break",
|
|
||||||
description: "Pause the client when you press F8 with DevTools (+ breakpoints) open.",
|
|
||||||
authors: [Devs.lewisakura],
|
|
||||||
|
|
||||||
start() {
|
|
||||||
window.addEventListener("keydown", this.event);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
window.removeEventListener("keydown", this.event);
|
|
||||||
},
|
|
||||||
|
|
||||||
event(e: KeyboardEvent) {
|
|
||||||
if (e.code === "F8") {
|
|
||||||
// Hi! You've just paused the client. Pressing F8 in DevTools or in the main window will unpause it again.
|
|
||||||
// It's up to you on what to do, friend. Happy travels!
|
|
||||||
debugger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -20,30 +20,12 @@ import { addPreEditListener, addPreSendListener, removePreEditListener, removePr
|
|||||||
import { migratePluginSettings, Settings } from "@api/settings";
|
import { migratePluginSettings, Settings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
||||||
import { getCurrentGuild } from "@utils/discord";
|
|
||||||
import { proxyLazy } from "@utils/proxyLazy";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
import { ChannelStore, FluxDispatcher, PermissionStore, UserStore } from "@webpack/common";
|
import { ChannelStore, PermissionStore, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
const DRAFT_TYPE = 0;
|
||||||
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
||||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
|
||||||
const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings");
|
|
||||||
const ReaderFactory = findByPropsLazy("readerFactory");
|
|
||||||
|
|
||||||
function searchProtoClass(localName: string, parentProtoClass: any) {
|
|
||||||
if (!parentProtoClass) return;
|
|
||||||
|
|
||||||
const field = parentProtoClass.fields.find(field => field.localName === localName);
|
|
||||||
if (!field) return;
|
|
||||||
|
|
||||||
const getter: any = Object.values(field).find(value => typeof value === "function");
|
|
||||||
return getter?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppearanceSettingsProto = proxyLazy(() => searchProtoClass("appearance", PreloadedUserSettingsProtoHandler.ProtoClass));
|
|
||||||
const ClientThemeSettingsProto = proxyLazy(() => searchProtoClass("clientThemeSettings", AppearanceSettingsProto));
|
|
||||||
|
|
||||||
const USE_EXTERNAL_EMOJIS = 1n << 18n;
|
const USE_EXTERNAL_EMOJIS = 1n << 18n;
|
||||||
const USE_EXTERNAL_STICKERS = 1n << 37n;
|
const USE_EXTERNAL_STICKERS = 1n << 37n;
|
||||||
@ -90,7 +72,7 @@ migratePluginSettings("FakeNitro", "NitroBypass");
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FakeNitro",
|
name: "FakeNitro",
|
||||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz],
|
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.",
|
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
@ -100,16 +82,16 @@ export default definePlugin({
|
|||||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=(\i)=\i\.intention)/,
|
match: /(?<=(?<intention>\i)=\i\.intention)/,
|
||||||
replace: (_, intention) => `,fakeNitroIntention=${intention}`
|
replace: ",fakeNitroIntention=$<intention>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
|
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g,
|
||||||
replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
|
replace: ",fakeNitroIntention"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
match: /(?<=&&!\i&&)!(?<canUseExternal>\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||||
replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
|
replace: `(!$<canUseExternal>&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -117,15 +99,15 @@ export default definePlugin({
|
|||||||
find: "canUseAnimatedEmojis:function",
|
find: "canUseAnimatedEmojis:function",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g,
|
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?<user>\i))\){(?<premiumCheck>.+?\))/g,
|
||||||
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
replace: `,fakeNitroIntention){$<premiumCheck>||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "canUseStickersEverywhere:function",
|
find: "canUseStickersEverywhere:function",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /canUseStickersEverywhere:function\(\i\){/,
|
match: /canUseStickersEverywhere:function\(.+?\{/,
|
||||||
replace: "$&return true;"
|
replace: "$&return true;"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -146,7 +128,7 @@ export default definePlugin({
|
|||||||
"canStreamMidQuality"
|
"canStreamMidQuality"
|
||||||
].map(func => {
|
].map(func => {
|
||||||
return {
|
return {
|
||||||
match: new RegExp(`${func}:function\\(\\i\\){`),
|
match: new RegExp(`${func}:function\\(.+?\\{`),
|
||||||
replace: "$&return true;"
|
replace: "$&return true;"
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -162,58 +144,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: "canUseClientThemes:function",
|
find: "canUseClientThemes:function",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /canUseClientThemes:function\(\i\){/,
|
match: /(?<=canUseClientThemes:function\(\i\){)/,
|
||||||
replace: "$&return true;"
|
replace: "return true;"
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '.displayName="UserSettingsProtoStore"',
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /CONNECTION_OPEN:function\((\i)\){/,
|
|
||||||
replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /=(\i)\.local;/,
|
|
||||||
replace: (m, props) => `${m}${props}.local||$self.handleProtoChange(${props}.settings.proto);`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "updateTheme:function",
|
|
||||||
replacement: {
|
|
||||||
match: /(function \i\(\i\){var (\i)=\i\.backgroundGradientPresetId.+?)(\i\.\i\.updateAsync.+?theme=(.+?);.+?\),\i\))/,
|
|
||||||
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: 'jumboable?"jumbo":"default"',
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
|
|
||||||
replacement: {
|
|
||||||
match: /jumboable\?"jumbo":"default",emojiId.+?}}\)},(?<=(\i)=function\(\i\){var \i=\i\.node.+?)/,
|
|
||||||
replace: (m, component) => `${m}fakeNitroEmojiComponentExport=($self.EmojiComponent=${component},void 0),`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /1!==(\i)\.length\|\|1!==\i\.length/,
|
|
||||||
replace: (m, content) => `${m}||${content}[0].target?.startsWith("https://cdn.discordapp.com/emojis/")`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
|
|
||||||
replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content},arguments[2]?.formatInline);`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "renderEmbeds=function",
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
|
|
||||||
replacement: {
|
|
||||||
match: /renderEmbeds=function\(\i\){.+?embeds\.map\(\(function\((\i)\){/,
|
|
||||||
replace: (m, embed) => `${m}if(${embed}.url?.startsWith("https://cdn.discordapp.com/emojis/"))return null;`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -231,12 +163,6 @@ export default definePlugin({
|
|||||||
default: 48,
|
default: 48,
|
||||||
markers: [32, 48, 64, 128, 160, 256, 512],
|
markers: [32, 48, 64, 128, 160, 256, 512],
|
||||||
},
|
},
|
||||||
transformEmojis: {
|
|
||||||
description: "Whether to transform fake emojis into real ones",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true,
|
|
||||||
},
|
|
||||||
enableStickerBypass: {
|
enableStickerBypass: {
|
||||||
description: "Allow sending fake stickers",
|
description: "Allow sending fake stickers",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
@ -258,7 +184,7 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
|
|
||||||
get guildId() {
|
get guildId() {
|
||||||
return getCurrentGuild()?.id;
|
return window.location.href.split("channels/")[1].split("/")[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
get canUseEmotes() {
|
get canUseEmotes() {
|
||||||
@ -269,101 +195,6 @@ export default definePlugin({
|
|||||||
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
|
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleProtoChange(proto: any, user: any) {
|
|
||||||
if ((!proto.appearance && !AppearanceSettingsProto) || !UserSettingsProtoStore) return;
|
|
||||||
|
|
||||||
const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;
|
|
||||||
|
|
||||||
if (premiumType !== 2) {
|
|
||||||
proto.appearance ??= AppearanceSettingsProto.create();
|
|
||||||
|
|
||||||
if (UserSettingsProtoStore.settings.appearance?.theme != null) {
|
|
||||||
proto.appearance.theme = UserSettingsProtoStore.settings.appearance.theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null && ClientThemeSettingsProto) {
|
|
||||||
const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({
|
|
||||||
backgroundGradientPresetId: {
|
|
||||||
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummyProto;
|
|
||||||
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) {
|
|
||||||
const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0;
|
|
||||||
if (premiumType === 2 || backgroundGradientPresetId == null) return original();
|
|
||||||
|
|
||||||
if (!AppearanceSettingsProto || !ClientThemeSettingsProto || !ReaderFactory) return;
|
|
||||||
|
|
||||||
const currentAppearanceProto = PreloadedUserSettingsProtoHandler.getCurrentValue().appearance;
|
|
||||||
|
|
||||||
const newAppearanceProto = currentAppearanceProto != null
|
|
||||||
? AppearanceSettingsProto.fromBinary(AppearanceSettingsProto.toBinary(currentAppearanceProto), ReaderFactory)
|
|
||||||
: AppearanceSettingsProto.create();
|
|
||||||
|
|
||||||
newAppearanceProto.theme = theme;
|
|
||||||
|
|
||||||
const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({
|
|
||||||
backgroundGradientPresetId: {
|
|
||||||
value: backgroundGradientPresetId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummyProto;
|
|
||||||
newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId;
|
|
||||||
|
|
||||||
const proto = PreloadedUserSettingsProtoHandler.ProtoClass.create();
|
|
||||||
proto.appearance = newAppearanceProto;
|
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
|
||||||
type: "USER_SETTINGS_PROTO_UPDATE",
|
|
||||||
local: true,
|
|
||||||
partial: true,
|
|
||||||
settings: {
|
|
||||||
type: 1,
|
|
||||||
proto
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
EmojiComponent: null as any,
|
|
||||||
|
|
||||||
patchFakeNitroEmojis(content: Array<any>, inline: boolean) {
|
|
||||||
if (!this.EmojiComponent) return content;
|
|
||||||
|
|
||||||
const newContent: Array<any> = [];
|
|
||||||
|
|
||||||
for (const element of content) {
|
|
||||||
if (element.props?.trusted == null) {
|
|
||||||
newContent.push(element);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeNitroMatch = element.props.href.match(/https:\/\/cdn\.discordapp\.com\/emojis\/(\d+?)\.(png|webp|gif).+?(?=\s|$)/);
|
|
||||||
if (!fakeNitroMatch) {
|
|
||||||
newContent.push(element);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
newContent.push((
|
|
||||||
<this.EmojiComponent node={{
|
|
||||||
type: "customEmoji",
|
|
||||||
jumboable: !inline && content.length === 1,
|
|
||||||
animated: fakeNitroMatch[2] === "gif",
|
|
||||||
name: ":FakeNitroEmoji:",
|
|
||||||
emojiId: fakeNitroMatch[1]
|
|
||||||
}} />
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return newContent;
|
|
||||||
},
|
|
||||||
|
|
||||||
hasPermissionToUseExternalEmojis(channelId: string) {
|
hasPermissionToUseExternalEmojis(channelId: string) {
|
||||||
const channel = ChannelStore.getChannel(channelId);
|
const channel = ChannelStore.getChannel(channelId);
|
||||||
|
|
@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { Forms } from "@webpack/common";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "FixInbox",
|
|
||||||
description: "Fixes the Unreads Inbox from crashing Discord when you're in lots of guilds.",
|
|
||||||
authors: [Devs.Megu],
|
|
||||||
|
|
||||||
patches: [{
|
|
||||||
find: "INBOX_OPEN:function",
|
|
||||||
replacement: {
|
|
||||||
// This function normally dispatches a subscribe event to every guild.
|
|
||||||
// this is badbadbadbadbad so we just get rid of it.
|
|
||||||
match: /INBOX_OPEN:function.+?\{/,
|
|
||||||
replace: "$&return true;"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
settingsAboutComponent() {
|
|
||||||
return (
|
|
||||||
<Forms.FormSection>
|
|
||||||
<Forms.FormTitle tag="h3">What's the problem?</Forms.FormTitle>
|
|
||||||
<Forms.FormText style={{ marginBottom: 8 }}>
|
|
||||||
By default, Discord emits a GUILD_SUBSCRIPTIONS event for every guild you're in.
|
|
||||||
When you're in a lot of guilds, this can cause the gateway to ratelimit you.
|
|
||||||
This causes the client to crash and get stuck in an infinite ratelimit loop as it tries to reconnect.
|
|
||||||
</Forms.FormText>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h3">How does it work?</Forms.FormTitle>
|
|
||||||
<Forms.FormText>
|
|
||||||
This plugin works by stopping the client from sending GUILD_SUBSCRIPTIONS events to the gateway when you open the unreads inbox.
|
|
||||||
This means that not all unreads will be shown, instead only already-subscribed guilds' unreads will be shown, but your client won't crash anymore.
|
|
||||||
</Forms.FormText>
|
|
||||||
</Forms.FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -23,7 +23,7 @@ import { findByProps } from "@webpack";
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FriendInvites",
|
name: "FriendInvites",
|
||||||
description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).",
|
description: "Generate and manage friend invite links.",
|
||||||
authors: [Devs.afn],
|
authors: [Devs.afn],
|
||||||
dependencies: ["CommandsAPI"],
|
dependencies: ["CommandsAPI"],
|
||||||
commands: [
|
commands: [
|
||||||
@ -37,8 +37,8 @@ export default definePlugin({
|
|||||||
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
content: `
|
content: `
|
||||||
discord.gg/${createInvite.code} ·
|
discord.gg/${createInvite.code}
|
||||||
Expires: <t:${new Date(createInvite.expires_at).getTime() / 1000}:R> ·
|
Expires: <t:${new Date(createInvite.expires_at).getTime() / 1000}:R>
|
||||||
Max uses: \`${createInvite.max_uses}\`
|
Max uses: \`${createInvite.max_uses}\`
|
||||||
`.trim().replace(/\s+/g, " ")
|
`.trim().replace(/\s+/g, " ")
|
||||||
});
|
});
|
||||||
@ -52,25 +52,25 @@ export default definePlugin({
|
|||||||
const friendInvites = findByProps("createFriendInvite");
|
const friendInvites = findByProps("createFriendInvite");
|
||||||
const invites = await friendInvites.getAllFriendInvites();
|
const invites = await friendInvites.getAllFriendInvites();
|
||||||
const friendInviteList = invites.map(i =>
|
const friendInviteList = invites.map(i =>
|
||||||
`_discord.gg/${i.code}_ ·
|
`_discord.gg/${i.code}_
|
||||||
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> ·
|
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R>
|
||||||
Times used: \`${i.uses}/${i.max_uses}\``.trim().replace(/\s+/g, " ")
|
Times used: \`${i.uses}/${i.max_uses}\``.trim().replace(/\s+/g, " ")
|
||||||
);
|
);
|
||||||
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
content: friendInviteList.join("\n") || "You have no active friend invites!"
|
content: friendInviteList.join("\n\n") || "You have no active friend invites!"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "revoke friend invites",
|
name: "revoke friend invites",
|
||||||
description: "Revokes all generated friend invites.",
|
description: "Revokes ALL generated friend invite links.",
|
||||||
inputType: ApplicationCommandInputType.BOT,
|
inputType: ApplicationCommandInputType.BOT,
|
||||||
execute: async (_, ctx) => {
|
execute: async (_, ctx) => {
|
||||||
await findByProps("createFriendInvite").revokeFriendInvites();
|
await findByProps("createFriendInvite").revokeFriendInvites();
|
||||||
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
content: "All friend invites have been revoked."
|
content: "All friend links have been revoked."
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { filters, findLazy, mapMangledModuleLazy } from "@webpack";
|
|
||||||
|
|
||||||
const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', {
|
|
||||||
close: filters.byCode("activeView:null", "setState")
|
|
||||||
});
|
|
||||||
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "GifPaste",
|
|
||||||
description: "Makes picking a gif in the gif picker insert a link into the chatbox instead of instantly sending it",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
|
|
||||||
patches: [{
|
|
||||||
find: ".handleSelectGIF=",
|
|
||||||
replacement: {
|
|
||||||
match: /\.handleSelectGIF=function.+?\{/,
|
|
||||||
replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
handleSelect(gif?: { url: string; }) {
|
|
||||||
if (gif) {
|
|
||||||
ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: gif.url + " " });
|
|
||||||
ExpressionPickerState.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -21,7 +21,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { useForceUpdater } from "@utils/misc";
|
import { useForceUpdater } from "@utils/misc";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Tooltip } from "webpack/common";
|
import { Tooltip } from "webpack/common";
|
||||||
|
|
||||||
enum ActivitiesTypes {
|
enum ActivitiesTypes {
|
||||||
@ -37,7 +37,7 @@ interface IgnoredActivity {
|
|||||||
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
|
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
|
||||||
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
|
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
|
||||||
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
|
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
|
||||||
const RunningGameStore = findStoreLazy("RunningGameStore");
|
const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen");
|
||||||
|
|
||||||
function ToggleIconOff() {
|
function ToggleIconOff() {
|
||||||
return (
|
return (
|
||||||
@ -71,7 +71,7 @@ function ToggleIconOff() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
|
function ToggleIconOn() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={RegisteredGamesClasses.overlayToggleIconOn}
|
className={RegisteredGamesClasses.overlayToggleIconOn}
|
||||||
@ -80,15 +80,14 @@ function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
|
|||||||
viewBox="0 0 32 26"
|
viewBox="0 0 32 26"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
className={forceWhite ? "" : RegisteredGamesClasses.fill}
|
className={RegisteredGamesClasses.fill}
|
||||||
fill={forceWhite ? "var(--white-500)" : ""}
|
|
||||||
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
|
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleActivityComponent({ activity, forceWhite }: { activity: IgnoredActivity; forceWhite?: boolean; }) {
|
function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) {
|
||||||
const forceUpdate = useForceUpdater();
|
const forceUpdate = useForceUpdater();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -106,7 +105,7 @@ function ToggleActivityComponent({ activity, forceWhite }: { activity: IgnoredAc
|
|||||||
{
|
{
|
||||||
ignoredActivitiesCache.has(activity.id)
|
ignoredActivitiesCache.has(activity.id)
|
||||||
? <ToggleIconOff />
|
? <ToggleIconOff />
|
||||||
: <ToggleIconOn forceWhite={forceWhite} />
|
: <ToggleIconOn />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -118,9 +117,9 @@ function ToggleActivityComponentWithBackground({ activity }: { activity: Ignored
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
|
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
|
||||||
style={{ padding: "0px 2px" }}
|
style={{ padding: "0 2px" }}
|
||||||
>
|
>
|
||||||
<ToggleActivityComponent activity={activity} forceWhite={true} />
|
<ToggleActivityComponent activity={activity} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -143,32 +142,25 @@ export default definePlugin({
|
|||||||
name: "IgnoreActivities",
|
name: "IgnoreActivities",
|
||||||
authors: [Devs.Nuckyz],
|
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.",
|
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: [
|
patches: [{
|
||||||
{
|
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
|
||||||
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
|
replacement: {
|
||||||
replacement: {
|
match: /var .=(?<props>.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/,
|
||||||
match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
|
replace: "$&,$self.renderToggleGameActivityButton($<props>)"
|
||||||
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false"
|
|
||||||
+ `${restWithoutPlatformCheck}`
|
|
||||||
+ `(${platformCheck}?${children}:[])`
|
|
||||||
+ `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".overlayBadge",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=\(\)\.badgeContainer.+?(\i)\.name}\):null)/,
|
|
||||||
replace: (_, props) => `,$self.renderToggleActivityButton(${props})`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '.displayName="LocalActivityStore"',
|
|
||||||
replacement: {
|
|
||||||
match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/,
|
|
||||||
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}, {
|
||||||
|
find: ".overlayBadge",
|
||||||
|
replacement: {
|
||||||
|
match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/,
|
||||||
|
replace: "$&,$self.renderToggleActivityButton($<props>)"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
find: '.displayName="LocalActivityStore"',
|
||||||
|
replacement: {
|
||||||
|
match: /(?<activities>.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?\)\)/,
|
||||||
|
replace: "$&;$<activities>=$<activities>.filter($self.isActivityNotIgnored);"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
|
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
|
||||||
@ -222,5 +214,5 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
@ -43,11 +43,8 @@ export function isPluginEnabled(p: string) {
|
|||||||
|
|
||||||
const pluginsValues = Object.values(Plugins);
|
const pluginsValues = Object.values(Plugins);
|
||||||
|
|
||||||
// First roundtrip to mark and force enable dependencies (only for enabled plugins)
|
// First roundtrip to mark and force enable dependencies
|
||||||
//
|
for (const p of pluginsValues) {
|
||||||
// FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only
|
|
||||||
// goes for the top level and their children, but for now this works okay with the current API plugins
|
|
||||||
for (const p of pluginsValues) if (settings[p.name]?.enabled) {
|
|
||||||
p.dependencies?.forEach(d => {
|
p.dependencies?.forEach(d => {
|
||||||
const dep = Plugins[d];
|
const dep = Plugins[d];
|
||||||
if (dep) {
|
if (dep) {
|
||||||
|
@ -91,8 +91,8 @@ function ChatBarIcon() {
|
|||||||
<svg
|
<svg
|
||||||
aria-hidden
|
aria-hidden
|
||||||
role="img"
|
role="img"
|
||||||
width="32"
|
width="24"
|
||||||
height="32"
|
height="24"
|
||||||
viewBox={"0 0 64 64"}
|
viewBox={"0 0 64 64"}
|
||||||
style={{ scale: "1.1" }}
|
style={{ scale: "1.1" }}
|
||||||
>
|
>
|
||||||
@ -131,8 +131,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".activeCommandOption",
|
find: ".activeCommandOption",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
|
match: /.=.\.activeCommand,.=.\.activeCommandOption,.{1,133}(.)=\[\];/,
|
||||||
replace: "$&;try{$2||$1.push($self.chatBarIcon())}catch{}",
|
replace: "$&;$1.push($self.chatBarIcon());",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -16,10 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
||||||
import { FluxDispatcher, Forms } from "@webpack/common";
|
import { FluxDispatcher, Forms } from "@webpack/common";
|
||||||
@ -31,17 +30,11 @@ interface ActivityAssets {
|
|||||||
small_text?: string;
|
small_text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ActivityButton {
|
|
||||||
label: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Activity {
|
interface Activity {
|
||||||
state: string;
|
state: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
timestamps?: {
|
timestamps?: {
|
||||||
start?: number;
|
start?: Number;
|
||||||
};
|
};
|
||||||
assets?: ActivityAssets;
|
assets?: ActivityAssets;
|
||||||
buttons?: Array<string>;
|
buttons?: Array<string>;
|
||||||
@ -50,8 +43,8 @@ interface Activity {
|
|||||||
metadata?: {
|
metadata?: {
|
||||||
button_urls?: Array<string>;
|
button_urls?: Array<string>;
|
||||||
};
|
};
|
||||||
type: number;
|
type: Number;
|
||||||
flags: number;
|
flags: Number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TrackData {
|
interface TrackData {
|
||||||
@ -73,9 +66,6 @@ enum ActivityFlag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const applicationId = "1043533871037284423";
|
const applicationId = "1043533871037284423";
|
||||||
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
|
||||||
|
|
||||||
const logger = new Logger("LastFMRichPresence");
|
|
||||||
|
|
||||||
const presenceStore = findByPropsLazy("getLocalPresence");
|
const presenceStore = findByPropsLazy("getLocalPresence");
|
||||||
const assetManager = mapMangledModuleLazy(
|
const assetManager = mapMangledModuleLazy(
|
||||||
@ -89,64 +79,14 @@ async function getApplicationAsset(key: string): Promise<string> {
|
|||||||
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
|
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActivity(activity: Activity | null) {
|
function setActivity(activity?: Activity) {
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: activity });
|
||||||
type: "LOCAL_ACTIVITY_UPDATE",
|
|
||||||
activity,
|
|
||||||
socketId: "LastFM",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
username: {
|
|
||||||
description: "last.fm username",
|
|
||||||
type: OptionType.STRING,
|
|
||||||
},
|
|
||||||
apiKey: {
|
|
||||||
description: "last.fm api key",
|
|
||||||
type: OptionType.STRING,
|
|
||||||
},
|
|
||||||
shareUsername: {
|
|
||||||
description: "show link to last.fm profile",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
hideWithSpotify: {
|
|
||||||
description: "hide last.fm presence if spotify is running",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
statusName: {
|
|
||||||
description: "text shown in status",
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "some music",
|
|
||||||
},
|
|
||||||
useListeningStatus: {
|
|
||||||
description: 'show "Listening to" status instead of "Playing"',
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
missingArt: {
|
|
||||||
description: "When album or album art is missing",
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "Use large Last.fm logo",
|
|
||||||
value: "lastfmLogo",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Use generic placeholder",
|
|
||||||
value: "placeholder"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "LastFMRichPresence",
|
name: "LastFMRichPresence",
|
||||||
description: "Little plugin for Last.fm rich presence",
|
description: "Little plugin for Last.fm rich presence",
|
||||||
authors: [Devs.dzshn, Devs.RuiNtD],
|
authors: [Devs.dzshn],
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
@ -164,9 +104,30 @@ export default definePlugin({
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
||||||
settings,
|
options: {
|
||||||
|
username: {
|
||||||
|
description: "last.fm username",
|
||||||
|
type: OptionType.STRING,
|
||||||
|
},
|
||||||
|
apiKey: {
|
||||||
|
description: "last.fm api key",
|
||||||
|
type: OptionType.STRING,
|
||||||
|
},
|
||||||
|
hideWithSpotify: {
|
||||||
|
description: "hide last.fm presence if spotify is running",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
useListeningStatus: {
|
||||||
|
description: 'show "Listening to" status instead of "Playing"',
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
this.settings = Settings.plugins.LastFMRichPresence;
|
||||||
|
|
||||||
this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000);
|
this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -175,112 +136,73 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fetchTrackData(): Promise<TrackData | null> {
|
async fetchTrackData(): Promise<TrackData | null> {
|
||||||
if (!settings.store.username || !settings.store.apiKey)
|
if (!this.settings.username || !this.settings.apiKey) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
try {
|
const response = await fetch(`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&api_key=${this.settings.apiKey}&user=${this.settings.username}&limit=1&format=json`);
|
||||||
const params = new URLSearchParams({
|
const trackData = (await response.json()).recenttracks.track[0];
|
||||||
method: "user.getrecenttracks",
|
|
||||||
api_key: settings.store.apiKey,
|
|
||||||
user: settings.store.username,
|
|
||||||
limit: "1",
|
|
||||||
format: "json"
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`);
|
if (!trackData["@attr"]?.nowplaying) return null;
|
||||||
if (!res.ok) throw `${res.status} ${res.statusText}`;
|
|
||||||
|
|
||||||
const json = await res.json();
|
// why does the json api have xml structure
|
||||||
if (json.error) {
|
return {
|
||||||
logger.error("Error from Last.fm API", `${json.error}: ${json.message}`);
|
name: trackData.name || "Unknown",
|
||||||
return null;
|
album: trackData.album["#text"],
|
||||||
}
|
artist: trackData.artist["#text"] || "Unknown",
|
||||||
|
url: trackData.url,
|
||||||
const trackData = json.recenttracks?.track[0];
|
imageUrl: (trackData.image || []).filter(x => x.size === "large")[0]?.["#text"]
|
||||||
|
};
|
||||||
if (!trackData || !trackData["@attr"]?.nowplaying)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// why does the json api have xml structure
|
|
||||||
return {
|
|
||||||
name: trackData.name || "Unknown",
|
|
||||||
album: trackData.album["#text"],
|
|
||||||
artist: trackData.artist["#text"] || "Unknown",
|
|
||||||
url: trackData.url,
|
|
||||||
imageUrl: trackData.image?.find((x: any) => x.size === "large")?.["#text"]
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to query Last.fm API", e);
|
|
||||||
// will clear the rich presence if API fails
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updatePresence() {
|
async updatePresence() {
|
||||||
setActivity(await this.getActivity());
|
if (this.settings.hideWithSpotify) {
|
||||||
},
|
|
||||||
|
|
||||||
getLargeImage(track: TrackData): string | undefined {
|
|
||||||
if (track.imageUrl && !track.imageUrl.includes(placeholderId))
|
|
||||||
return track.imageUrl;
|
|
||||||
|
|
||||||
if (settings.store.missingArt === "placeholder")
|
|
||||||
return "placeholder";
|
|
||||||
},
|
|
||||||
|
|
||||||
async getActivity(): Promise<Activity | null> {
|
|
||||||
if (settings.store.hideWithSpotify) {
|
|
||||||
for (const activity of presenceStore.getActivities()) {
|
for (const activity of presenceStore.getActivities()) {
|
||||||
if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) {
|
if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) {
|
||||||
// there is already music status because of Spotify or richerCider (probably more)
|
// there is already music status (probably only spotify can do this currently)
|
||||||
return null;
|
setActivity();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackData = await this.fetchTrackData();
|
const trackData = await this.fetchTrackData();
|
||||||
if (!trackData) return null;
|
|
||||||
|
|
||||||
const largeImage = this.getLargeImage(trackData);
|
if (!trackData) {
|
||||||
const assets: ActivityAssets = largeImage ?
|
setActivity();
|
||||||
{
|
return;
|
||||||
large_image: await getApplicationAsset(largeImage),
|
}
|
||||||
large_text: trackData.album || undefined,
|
|
||||||
|
const hideAlbumName = !trackData.album || trackData.album === trackData.name;
|
||||||
|
|
||||||
|
let assets: ActivityAssets;
|
||||||
|
if (trackData.imageUrl) {
|
||||||
|
assets = {
|
||||||
|
large_image: await getApplicationAsset(trackData.imageUrl),
|
||||||
|
large_text: trackData.name,
|
||||||
small_image: await getApplicationAsset("lastfm-small"),
|
small_image: await getApplicationAsset("lastfm-small"),
|
||||||
small_text: "Last.fm",
|
small_text: "Last.fm",
|
||||||
} : {
|
|
||||||
large_image: await getApplicationAsset("lastfm-large"),
|
|
||||||
large_text: trackData.album || undefined,
|
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
assets = {
|
||||||
|
large_image: await getApplicationAsset("lastfm-large"),
|
||||||
|
large_text: "Last.fm",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const buttons: ActivityButton[] = [
|
setActivity({
|
||||||
{
|
|
||||||
label: "View Song",
|
|
||||||
url: trackData.url,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (settings.store.shareUsername)
|
|
||||||
buttons.push({
|
|
||||||
label: "Last.fm Profile",
|
|
||||||
url: `https://www.last.fm/user/${settings.store.username}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
application_id: applicationId,
|
application_id: applicationId,
|
||||||
name: settings.store.statusName,
|
name: "some music",
|
||||||
|
|
||||||
details: trackData.name,
|
details: trackData.name,
|
||||||
state: trackData.artist,
|
state: hideAlbumName ? trackData.artist : `${trackData.artist} - ${trackData.album}`,
|
||||||
assets,
|
assets,
|
||||||
|
|
||||||
buttons: buttons.map(v => v.label),
|
buttons: ["Open in Last.fm"],
|
||||||
metadata: {
|
metadata: {
|
||||||
button_urls: buttons.map(v => v.url),
|
button_urls: [trackData.url]
|
||||||
},
|
},
|
||||||
|
|
||||||
type: settings.store.useListeningStatus ? ActivityType.LISTENING : ActivityType.PLAYING,
|
type: this.settings.useListeningStatus ? ActivityType.LISTENING : ActivityType.PLAYING,
|
||||||
flags: ActivityFlag.INSTANCE,
|
flags: ActivityFlag.INSTANCE,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -20,20 +20,18 @@ import { addClickListener, removeClickListener } from "@api/MessageEvents";
|
|||||||
import { migratePluginSettings } from "@api/settings";
|
import { migratePluginSettings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy, findLazy } from "@webpack";
|
||||||
import { PermissionStore, UserStore } from "@webpack/common";
|
import { UserStore } from "@webpack/common";
|
||||||
|
|
||||||
let isDeletePressed = false;
|
let isDeletePressed = false;
|
||||||
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
||||||
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
||||||
|
|
||||||
const MANAGE_CHANNELS = 1n << 4n;
|
|
||||||
|
|
||||||
migratePluginSettings("MessageClickActions", "MessageQuickActions");
|
migratePluginSettings("MessageClickActions", "MessageQuickActions");
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageClickActions",
|
name: "MessageClickActions",
|
||||||
description: "Hold Backspace and click to delete, double click to edit",
|
description: "Hold Delete and click to delete, double click to edit",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
@ -52,6 +50,8 @@ export default definePlugin({
|
|||||||
|
|
||||||
start() {
|
start() {
|
||||||
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||||
|
const PermissionStore = findByPropsLazy("can", "initialize");
|
||||||
|
const Permissions = findLazy(m => typeof m.MANAGE_MESSAGES === "bigint");
|
||||||
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||||
|
|
||||||
document.addEventListener("keydown", keydown);
|
document.addEventListener("keydown", keydown);
|
||||||
@ -64,7 +64,7 @@ export default definePlugin({
|
|||||||
MessageActions.startEditMessage(chan.id, msg.id, msg.content);
|
MessageActions.startEditMessage(chan.id, msg.id, msg.content);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
} else if (Vencord.Settings.plugins.MessageClickActions.enableDeleteOnClick && (isMe || PermissionStore.can(MANAGE_CHANNELS, chan))) {
|
} else if (Vencord.Settings.plugins.MessageClickActions.enableDeleteOnClick && (isMe || PermissionStore.can(Permissions.MANAGE_MESSAGES, chan))) {
|
||||||
MessageActions.deleteMessage(chan.id, msg.id);
|
MessageActions.deleteMessage(chan.id, msg.id);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
@ -17,13 +17,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { addAccessory } from "@api/MessageAccessories";
|
import { addAccessory } from "@api/MessageAccessories";
|
||||||
import { definePluginSettings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants.js";
|
import { Devs } from "@utils/constants.js";
|
||||||
import { classes, LazyComponent } from "@utils/misc";
|
|
||||||
import { Queue } from "@utils/Queue";
|
import { Queue } from "@utils/Queue";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { find, findByCode, findByPropsLazy } from "@webpack";
|
import { filters, findByPropsLazy, waitFor } from "@webpack";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ChannelStore,
|
ChannelStore,
|
||||||
@ -38,20 +36,41 @@ import {
|
|||||||
} from "@webpack/common";
|
} from "@webpack/common";
|
||||||
import { Channel, Guild, Message } from "discord-types/general";
|
import { Channel, Guild, Message } from "discord-types/general";
|
||||||
|
|
||||||
const messageCache = new Map<string, {
|
let messageCache: { [id: string]: { message?: Message, fetched: boolean; }; } = {};
|
||||||
message?: Message;
|
|
||||||
fetched: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed"));
|
let AutomodEmbed: React.ComponentType<any>,
|
||||||
const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes('["message","compact","className",')));
|
Embed: React.ComponentType<any>,
|
||||||
|
ChannelMessage: React.ComponentType<any>,
|
||||||
|
Endpoints: Record<string, any>;
|
||||||
|
|
||||||
|
waitFor(["mle_AutomodEmbed"], m => (AutomodEmbed = m.mle_AutomodEmbed));
|
||||||
|
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");
|
const SearchResultClasses = findByPropsLazy("message", "searchResult");
|
||||||
|
|
||||||
let AutoModEmbed: React.ComponentType<any> = () => null;
|
const messageFetchQueue = new Queue();
|
||||||
|
async function fetchMessage(channelID: string, messageID: string): Promise<Message | void> {
|
||||||
|
if (messageID in messageCache && !messageCache[messageID].fetched) return Promise.resolve();
|
||||||
|
if (messageCache[messageID]?.fetched) return Promise.resolve(messageCache[messageID].message);
|
||||||
|
|
||||||
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
|
messageCache[messageID] = { fetched: false };
|
||||||
const tenorRegex = /https:\/\/(?:www.)?tenor\.com/;
|
const res = await RestAPI.get({
|
||||||
|
url: Endpoints.MESSAGES(channelID),
|
||||||
|
query: {
|
||||||
|
limit: 1,
|
||||||
|
around: messageID
|
||||||
|
},
|
||||||
|
retries: 2
|
||||||
|
}).catch(() => { });
|
||||||
|
const apiMessage = res.body?.[0];
|
||||||
|
const message: Message = MessageStore.getMessages(apiMessage.channel_id).receiveMessage(apiMessage).get(apiMessage.id);
|
||||||
|
messageCache[message.id] = {
|
||||||
|
message: message,
|
||||||
|
fetched: true
|
||||||
|
};
|
||||||
|
return Promise.resolve(message);
|
||||||
|
}
|
||||||
|
|
||||||
interface Attachment {
|
interface Attachment {
|
||||||
height: number;
|
height: number;
|
||||||
@ -60,131 +79,64 @@ interface Attachment {
|
|||||||
proxyURL?: string;
|
proxyURL?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageEmbedProps {
|
const isTenorGif = /https:\/\/(?:www.)?tenor\.com/;
|
||||||
message: Message;
|
|
||||||
channel: Channel;
|
|
||||||
guildID: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageFetchQueue = new Queue();
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
messageBackgroundColor: {
|
|
||||||
description: "Background color for messages in rich embeds",
|
|
||||||
type: OptionType.BOOLEAN
|
|
||||||
},
|
|
||||||
automodEmbeds: {
|
|
||||||
description: "Use automod embeds instead of rich embeds (smaller but less info)",
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "Always use automod embeds",
|
|
||||||
value: "always"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Prefer automod embeds, but use rich embeds if some content can't be shown",
|
|
||||||
value: "prefer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Never use automod embeds",
|
|
||||||
value: "never",
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
clearMessageCache: {
|
|
||||||
type: OptionType.COMPONENT,
|
|
||||||
description: "Clear the linked message cache",
|
|
||||||
component: () =>
|
|
||||||
<Button onClick={() => messageCache.clear()}>
|
|
||||||
Clear the linked message cache
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
async function fetchMessage(channelID: string, messageID: string) {
|
|
||||||
const cached = messageCache.get(messageID);
|
|
||||||
if (cached) return cached.message;
|
|
||||||
|
|
||||||
messageCache.set(messageID, { fetched: false });
|
|
||||||
|
|
||||||
const res = await RestAPI.get({
|
|
||||||
url: `/channels/${channelID}/messages`,
|
|
||||||
query: {
|
|
||||||
limit: 1,
|
|
||||||
around: messageID
|
|
||||||
},
|
|
||||||
retries: 2
|
|
||||||
}).catch(() => null);
|
|
||||||
|
|
||||||
const msg = res?.body?.[0];
|
|
||||||
if (!msg) return;
|
|
||||||
|
|
||||||
const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
|
|
||||||
|
|
||||||
messageCache.set(message.id, {
|
|
||||||
message,
|
|
||||||
fetched: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getImages(message: Message): Attachment[] {
|
function getImages(message: Message): Attachment[] {
|
||||||
const attachments: Attachment[] = [];
|
const attachments: Attachment[] = [];
|
||||||
|
message.attachments?.forEach(a => {
|
||||||
for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) {
|
if (a.content_type!.startsWith("image/")) attachments.push({
|
||||||
if (content_type?.startsWith("image/"))
|
height: a.height!,
|
||||||
|
width: a.width!,
|
||||||
|
url: a.url,
|
||||||
|
proxyURL: a.proxy_url!
|
||||||
|
});
|
||||||
|
});
|
||||||
|
message.embeds?.forEach(e => {
|
||||||
|
if (e.type === "image") attachments.push(
|
||||||
|
e.image ? { ...e.image } : { ...e.thumbnail! }
|
||||||
|
);
|
||||||
|
if (e.type === "gifv" && !isTenorGif.test(e.url!)) {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
height: height!,
|
height: e.thumbnail!.height,
|
||||||
width: width!,
|
width: e.thumbnail!.width,
|
||||||
url: url,
|
url: e.url!
|
||||||
proxyURL: proxy_url!
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
for (const { type, image, thumbnail, url } of message.embeds ?? []) {
|
|
||||||
if (type === "image")
|
|
||||||
attachments.push({ ...(image ?? thumbnail!) });
|
|
||||||
else if (url && type === "gifv" && !tenorRegex.test(url))
|
|
||||||
attachments.push({
|
|
||||||
height: thumbnail!.height,
|
|
||||||
width: thumbnail!.width,
|
|
||||||
url
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return attachments;
|
return attachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noContent(attachments: number, embeds: number) {
|
const noContent = (attachments: number, embeds: number): string => {
|
||||||
if (!attachments && !embeds) return "";
|
if (!attachments && !embeds) return "";
|
||||||
if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
||||||
if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`;
|
if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`;
|
||||||
return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
||||||
}
|
};
|
||||||
|
|
||||||
function requiresRichEmbed(message: Message) {
|
function requiresRichEmbed(message: Message) {
|
||||||
if (message.components.length) return true;
|
if (message.attachments.every(a => a.content_type?.startsWith("image/"))
|
||||||
if (message.attachments.some(a => !a.content_type?.startsWith("image/"))) return true;
|
&& message.embeds.every(e => e.type === "image" || (e.type === "gifv" && !isTenorGif.test(e.url!)))
|
||||||
if (message.embeds.some(e => e.type !== "image" && (e.type !== "gifv" || tenorRegex.test(e.url!)))) return true;
|
&& !message.components.length
|
||||||
|
) return false;
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeWidthAndHeight(width: number, height: number) {
|
const computeWidthAndHeight = (width: number, height: number) => {
|
||||||
const maxWidth = 400;
|
const maxWidth = 400, maxHeight = 300;
|
||||||
const maxHeight = 300;
|
let newWidth: number, newHeight: number;
|
||||||
|
|
||||||
if (width > height) {
|
if (width > height) {
|
||||||
const adjustedWidth = Math.min(width, maxWidth);
|
newWidth = Math.min(width, maxWidth);
|
||||||
return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) };
|
newHeight = Math.round(height / (width / newWidth));
|
||||||
|
} else {
|
||||||
|
newHeight = Math.min(height, maxHeight);
|
||||||
|
newWidth = Math.round(width / (height / newHeight));
|
||||||
}
|
}
|
||||||
|
return { width: newWidth, height: newHeight };
|
||||||
|
};
|
||||||
|
|
||||||
const adjustedHeight = Math.min(height, maxHeight);
|
interface MessageEmbedProps {
|
||||||
return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight };
|
message: Message;
|
||||||
|
channel: Channel;
|
||||||
|
guildID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
||||||
@ -197,172 +149,181 @@ function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function MessageEmbedAccessory({ message }: { message: Message; }) {
|
|
||||||
// @ts-ignore
|
|
||||||
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
|
|
||||||
|
|
||||||
const accessories = [] as (JSX.Element | null)[];
|
|
||||||
|
|
||||||
let match = null as RegExpMatchArray | null;
|
|
||||||
while ((match = 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.get(messageID)?.message;
|
|
||||||
if (!linkedMessage) {
|
|
||||||
linkedMessage ??= MessageStore.getMessage(channelID, messageID);
|
|
||||||
if (linkedMessage) {
|
|
||||||
messageCache.set(messageID, { message: linkedMessage, fetched: true });
|
|
||||||
} else {
|
|
||||||
const msg = { ...message } as any;
|
|
||||||
delete msg.embeds;
|
|
||||||
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
|
|
||||||
.then(m => m && FluxDispatcher.dispatch({
|
|
||||||
type: "MESSAGE_UPDATE",
|
|
||||||
message: msg
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageProps: MessageEmbedProps = {
|
|
||||||
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
|
|
||||||
channel: linkedChannel,
|
|
||||||
guildID
|
|
||||||
};
|
|
||||||
|
|
||||||
const type = settings.store.automodEmbeds;
|
|
||||||
accessories.push(
|
|
||||||
type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
|
|
||||||
? <AutomodEmbedAccessory {...messageProps} />
|
|
||||||
: <ChannelMessageEmbedAccessory {...messageProps} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessories.length ? <>{accessories}</> : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null {
|
|
||||||
const isDM = guildID === "@me";
|
|
||||||
|
|
||||||
const guild = !isDM && GuildStore.getGuild(channel.guild_id);
|
|
||||||
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);
|
|
||||||
|
|
||||||
|
|
||||||
return <Embed
|
|
||||||
embed={{
|
|
||||||
rawDescription: "",
|
|
||||||
color: "var(--background-secondary)",
|
|
||||||
author: {
|
|
||||||
name: <Text variant="text-xs/medium" tag="span">
|
|
||||||
<span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>
|
|
||||||
{isDM
|
|
||||||
? Parser.parse(`<@${dmReceiver.id}>`)
|
|
||||||
: Parser.parse(`<#${channel.id}>`)
|
|
||||||
}
|
|
||||||
</Text>,
|
|
||||||
iconProxyURL: guild
|
|
||||||
? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png`
|
|
||||||
: `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}`
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
renderDescription={() => (
|
|
||||||
<div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
|
|
||||||
<ChannelMessage
|
|
||||||
id={`message-link-embeds-${message.id}`}
|
|
||||||
message={message}
|
|
||||||
channel={channel}
|
|
||||||
subscribeToComponentDispatch={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
|
||||||
const { message, channel, guildID } = props;
|
|
||||||
|
|
||||||
const isDM = guildID === "@me";
|
|
||||||
const images = getImages(message);
|
|
||||||
const { parse } = Parser;
|
|
||||||
|
|
||||||
return <AutoModEmbed
|
|
||||||
channel={channel}
|
|
||||||
childrenAccessories={
|
|
||||||
<Text color="text-muted" variant="text-xs/medium" tag="span">
|
|
||||||
{isDM
|
|
||||||
? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
|
|
||||||
: parse(`<#${channel.id}>`)
|
|
||||||
}
|
|
||||||
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
compact={false}
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
{message.content || message.attachments.length <= images.length
|
|
||||||
? parse(message.content)
|
|
||||||
: [noContent(message.attachments.length, message.embeds.length)]
|
|
||||||
}
|
|
||||||
{images.map(a => {
|
|
||||||
const { width, height } = computeWidthAndHeight(a.width, a.height);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<img src={a.url} width={width} height={height} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
hideTimestamp={false}
|
|
||||||
message={message}
|
|
||||||
_messageEmbed="automod"
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageLinkEmbeds",
|
name: "MessageLinkEmbeds",
|
||||||
description: "Adds a preview to messages that link another message",
|
description: "Adds a preview to messages that link another message",
|
||||||
authors: [Devs.TheSun, Devs.Ven],
|
authors: [Devs.TheSun],
|
||||||
dependencies: ["MessageAccessoriesAPI"],
|
dependencies: ["MessageAccessoriesAPI"],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".embedCard",
|
find: ".embedCard",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /function (\i)\(\i\){var \i=\i\.message,\i=\i\.channel.{0,200}\.hideTimestamp/,
|
match: /{"use strict";(.{0,10})\(\)=>(.{1,2})}\);/,
|
||||||
replace: "$self.AutoModEmbed=$1;$&"
|
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: "$&;var messageEmbed={mle_AutomodEmbed:$1};"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
options: {
|
||||||
set AutoModEmbed(e: any) {
|
messageBackgroundColor: {
|
||||||
AutoModEmbed = e;
|
description: "Background color for messages in rich embeds",
|
||||||
|
type: OptionType.BOOLEAN
|
||||||
|
},
|
||||||
|
automodEmbeds: {
|
||||||
|
description: "Use automod embeds instead of rich embeds (smaller but less info)",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [{
|
||||||
|
label: "Always use automod embeds",
|
||||||
|
value: "always"
|
||||||
|
}, {
|
||||||
|
label: "Prefer automod embeds, but use rich embeds if some content can't be shown",
|
||||||
|
value: "prefer"
|
||||||
|
}, {
|
||||||
|
label: "Never use automod embeds",
|
||||||
|
value: "never",
|
||||||
|
default: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
clearMessageCache: {
|
||||||
|
type: OptionType.COMPONENT,
|
||||||
|
description: "Clear the linked message cache",
|
||||||
|
component: () =>
|
||||||
|
<Button onClick={() => messageCache = {}}>
|
||||||
|
Clear the linked message cache
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addAccessory("messageLinkEmbed", props => {
|
addAccessory("messageLinkEmbed", props => this.messageEmbedAccessory(props), 4 /* just above rich embeds*/);
|
||||||
if (!messageLinkRegex.test(props.message.content))
|
},
|
||||||
return null;
|
|
||||||
|
|
||||||
// need to reset the regex because it's global
|
messageLinkRegex: /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,19}|@me)\/(\d{17,19})\/(\d{17,19})/g,
|
||||||
messageLinkRegex.lastIndex = 0;
|
|
||||||
|
|
||||||
return (
|
messageEmbedAccessory(props) {
|
||||||
<ErrorBoundary>
|
const { message }: { message: Message; } = props;
|
||||||
<MessageEmbedAccessory message={props.message} />
|
// @ts-ignore
|
||||||
</ErrorBoundary>
|
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;
|
||||||
|
if (!linkedMessage) {
|
||||||
|
linkedMessage ??= MessageStore.getMessage(channelID, messageID);
|
||||||
|
if (linkedMessage) messageCache[messageID] = { message: linkedMessage, fetched: true };
|
||||||
|
else {
|
||||||
|
const msg = { ...message } as any;
|
||||||
|
delete msg.embeds;
|
||||||
|
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
|
||||||
|
.then(m => m && FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_UPDATE",
|
||||||
|
message: msg
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const messageProps: MessageEmbedProps = {
|
||||||
|
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
|
||||||
|
channel: linkedChannel,
|
||||||
|
guildID
|
||||||
|
};
|
||||||
|
|
||||||
|
const type = Settings.plugins[this.name].automodEmbeds;
|
||||||
|
accessories.push(
|
||||||
|
type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
|
||||||
|
? this.automodEmbedAccessory(messageProps)
|
||||||
|
: this.channelMessageEmbedAccessory(messageProps)
|
||||||
);
|
);
|
||||||
}, 4 /* just above rich embeds */);
|
}
|
||||||
|
return accessories;
|
||||||
|
},
|
||||||
|
|
||||||
|
channelMessageEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||||
|
const { message, channel, guildID } = props;
|
||||||
|
|
||||||
|
const isDM = guildID === "@me";
|
||||||
|
const guild = !isDM && GuildStore.getGuild(channel.guild_id);
|
||||||
|
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);
|
||||||
|
const classNames = [SearchResultClasses.message];
|
||||||
|
if (Settings.plugins[this.name].messageBackgroundColor) classNames.push(SearchResultClasses.searchResult);
|
||||||
|
|
||||||
|
return <Embed
|
||||||
|
embed={{
|
||||||
|
rawDescription: "",
|
||||||
|
color: "var(--background-secondary)",
|
||||||
|
author: {
|
||||||
|
name: <Text variant="text-xs/medium" tag="span">
|
||||||
|
{[
|
||||||
|
<span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>,
|
||||||
|
...(isDM
|
||||||
|
? Parser.parse(`<@${dmReceiver.id}>`)
|
||||||
|
: Parser.parse(`<#${channel.id}>`)
|
||||||
|
)
|
||||||
|
]}
|
||||||
|
</Text>,
|
||||||
|
iconProxyURL: guild
|
||||||
|
? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png`
|
||||||
|
: `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}`
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderDescription={() => {
|
||||||
|
return <div key={message.id} className={classNames.join(" ")}>
|
||||||
|
<ChannelMessage
|
||||||
|
id={`message-link-embeds-${message.id}`}
|
||||||
|
message={message}
|
||||||
|
channel={channel}
|
||||||
|
subscribeToComponentDispatch={false}
|
||||||
|
/>
|
||||||
|
</div >;
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
},
|
||||||
|
|
||||||
|
automodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||||
|
const { message, channel, guildID } = props;
|
||||||
|
|
||||||
|
const isDM = guildID === "@me";
|
||||||
|
const images = getImages(message);
|
||||||
|
const { parse } = Parser;
|
||||||
|
|
||||||
|
return <AutomodEmbed
|
||||||
|
channel={channel}
|
||||||
|
childrenAccessories={<Text color="text-muted" variant="text-xs/medium" tag="span">
|
||||||
|
{[
|
||||||
|
...(isDM ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) : parse(`<#${channel.id}>`)),
|
||||||
|
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
||||||
|
]}
|
||||||
|
</Text>}
|
||||||
|
compact={false}
|
||||||
|
content={[
|
||||||
|
...(message.content || !(message.attachments.length > images.length)
|
||||||
|
? parse(message.content)
|
||||||
|
: [noContent(message.attachments.length, message.embeds.length)]
|
||||||
|
),
|
||||||
|
...(images.map<JSX.Element>(a => {
|
||||||
|
const { width, height } = computeWidthAndHeight(a.width, a.height);
|
||||||
|
return <div><img src={a.url} width={width} height={height} /></div>;
|
||||||
|
}
|
||||||
|
))
|
||||||
|
]}
|
||||||
|
hideTimestamp={false}
|
||||||
|
message={message}
|
||||||
|
_messageEmbed="automod"
|
||||||
|
/>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
.messagelogger-deleted div {
|
.messagelogger-deleted div {
|
||||||
color: #f04747;
|
color: #f04747;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagelogger-deleted a {
|
|
||||||
color: #be3535;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
@ -18,21 +18,17 @@
|
|||||||
|
|
||||||
import "./messageLogger.css";
|
import "./messageLogger.css";
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
|
||||||
import { Settings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
import { disableStyle, enableStyle } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { moment, Parser, Timestamp, UserStore } from "@webpack/common";
|
||||||
import { FluxDispatcher, i18n, Menu, moment, Parser, Timestamp, UserStore } from "@webpack/common";
|
|
||||||
|
|
||||||
import overlayStyle from "./deleteStyleOverlay.css?managed";
|
import overlayStyle from "./deleteStyleOverlay.css?managed";
|
||||||
import textStyle from "./deleteStyleText.css?managed";
|
import textStyle from "./deleteStyleText.css?managed";
|
||||||
|
|
||||||
const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessage");
|
|
||||||
|
|
||||||
function addDeleteStyle() {
|
function addDeleteStyle() {
|
||||||
if (Settings.plugins.MessageLogger.deleteStyle === "text") {
|
if (Settings.plugins.MessageLogger.deleteStyle === "text") {
|
||||||
enableStyle(textStyle);
|
enableStyle(textStyle);
|
||||||
@ -43,48 +39,20 @@ function addDeleteStyle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MENU_ITEM_ID = "message-logger-remove-history";
|
|
||||||
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => {
|
|
||||||
const { message } = props;
|
|
||||||
const { deleted, editHistory, id, channel_id } = message;
|
|
||||||
|
|
||||||
if (!deleted && !editHistory?.length) return;
|
|
||||||
if (children.some(c => c?.props?.id === MENU_ITEM_ID)) return;
|
|
||||||
|
|
||||||
children.push((
|
|
||||||
<Menu.MenuItem
|
|
||||||
id={MENU_ITEM_ID}
|
|
||||||
key={MENU_ITEM_ID}
|
|
||||||
label="Remove Message History"
|
|
||||||
action={() => {
|
|
||||||
if (message.deleted) {
|
|
||||||
FluxDispatcher.dispatch({
|
|
||||||
type: "MESSAGE_DELETE",
|
|
||||||
channelId: channel_id,
|
|
||||||
id,
|
|
||||||
mlDeleted: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.editHistory = [];
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageLogger",
|
name: "MessageLogger",
|
||||||
description: "Temporarily logs deleted and edited messages.",
|
description: "Temporarily logs deleted and edited messages.",
|
||||||
authors: [Devs.rushii, Devs.Ven],
|
authors: [Devs.rushii, Devs.Ven],
|
||||||
dependencies: ["ContextMenuAPI", "MenuItemDeobfuscatorAPI"],
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addDeleteStyle();
|
addDeleteStyle();
|
||||||
addContextMenuPatch("message", patchMessageContextMenu);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
removeContextMenuPatch("message", patchMessageContextMenu);
|
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; }) {
|
renderEdit(edit: { timestamp: any, content: string; }) {
|
||||||
@ -97,7 +65,7 @@ export default definePlugin({
|
|||||||
isEdited={true}
|
isEdited={true}
|
||||||
isInline={false}
|
isInline={false}
|
||||||
>
|
>
|
||||||
<span className={styles.edited}>{" "}({i18n.Messages.MESSAGE_EDITED})</span>
|
<span>{" "}(edited)</span>
|
||||||
</Timestamp>
|
</Timestamp>
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@ -134,7 +102,7 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDelete(cache: any, data: { ids: string[], id: string; mlDeleted?: boolean; }, isBulk: boolean) {
|
handleDelete(cache: any, data: { ids: string[], id: string; }, isBulk: boolean) {
|
||||||
try {
|
try {
|
||||||
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
|
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
|
||||||
|
|
||||||
@ -146,8 +114,7 @@ export default definePlugin({
|
|||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
const EPHEMERAL = 64;
|
const EPHEMERAL = 64;
|
||||||
const shouldIgnore = data.mlDeleted ||
|
const shouldIgnore = (msg.flags & EPHEMERAL) === EPHEMERAL ||
|
||||||
(msg.flags & EPHEMERAL) === EPHEMERAL ||
|
|
||||||
ignoreBots && msg.author?.bot ||
|
ignoreBots && msg.author?.bot ||
|
||||||
ignoreSelf && msg.author?.id === myId;
|
ignoreSelf && msg.author?.id === myId;
|
||||||
|
|
||||||
@ -203,7 +170,6 @@ export default definePlugin({
|
|||||||
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
|
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
|
||||||
replace: "$1" +
|
replace: "$1" +
|
||||||
".update($3,m =>" +
|
".update($3,m =>" +
|
||||||
" (($2.message.flags & 64) === 64 || (Vencord.Settings.plugins.MessageLogger.ignoreBots && $2.message.author?.bot) || (Vencord.Settings.plugins.MessageLogger.ignoreSelf && $2.message.author?.id === Vencord.Webpack.Common.UserStore.getCurrentUser().id)) ? m :" +
|
|
||||||
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
||||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
||||||
" m" +
|
" m" +
|
||||||
@ -300,10 +266,15 @@ export default definePlugin({
|
|||||||
// Module 748241
|
// Module 748241
|
||||||
find: "Message must not be a thread starter message",
|
find: "Message must not be a thread starter message",
|
||||||
replacement: [
|
replacement: [
|
||||||
|
{
|
||||||
|
// Write message.deleted to deleted var
|
||||||
|
match: /var (\w)=(\w).id,(?=\w=\w.message)/,
|
||||||
|
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:/,
|
match: /\)\("li",\{(.+?),className:/,
|
||||||
replace: ")(\"li\",{$1,className:(arguments[0].message.deleted ? \"messagelogger-deleted \" : \"\")+"
|
replace: ")(\"li\",{$1,className:(deleted ? \"messagelogger-deleted \" : \"\")+"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2,15 +2,13 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagelogger-deleted-attachment,
|
.messagelogger-deleted-attachment {
|
||||||
.messagelogger-deleted div iframe {
|
|
||||||
filter: grayscale(1);
|
filter: grayscale(1);
|
||||||
transition: 150ms filter ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagelogger-deleted-attachment:hover,
|
.messagelogger-deleted-attachment:hover {
|
||||||
.messagelogger-deleted div iframe:hover {
|
|
||||||
filter: grayscale(0);
|
filter: grayscale(0);
|
||||||
|
transition: 250ms filter linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-dark .messagelogger-edited {
|
.theme-dark .messagelogger-edited {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent";
|
import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { sleep } from "@utils/misc";
|
import { sleep } from "@utils/misc";
|
||||||
@ -54,36 +54,15 @@ const MOYAI = "🗿";
|
|||||||
const MOYAI_URL =
|
const MOYAI_URL =
|
||||||
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
volume: {
|
|
||||||
description: "Volume of the 🗿🗿🗿",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
markers: makeRange(0, 1, 0.1),
|
|
||||||
default: 0.5,
|
|
||||||
stickToMarkers: false
|
|
||||||
},
|
|
||||||
triggerWhenUnfocused: {
|
|
||||||
description: "Trigger the 🗿 even when the window is unfocused",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
ignoreBots: {
|
|
||||||
description: "Ignore bots",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Moyai",
|
name: "Moyai",
|
||||||
authors: [Devs.Megu, Devs.Nuckyz],
|
authors: [Devs.Megu, Devs.Nuckyz],
|
||||||
description: "🗿🗿🗿🗿🗿🗿🗿🗿",
|
description: "🗿🗿🗿🗿🗿🗿🗿🗿",
|
||||||
settings,
|
|
||||||
|
|
||||||
async onMessage(e: IMessageCreate) {
|
async onMessage(e: IMessageCreate) {
|
||||||
if (e.optimistic || e.type !== "MESSAGE_CREATE") return;
|
if (e.optimistic || e.type !== "MESSAGE_CREATE") return;
|
||||||
if (e.message.state === "SENDING") return;
|
if (e.message.state === "SENDING") return;
|
||||||
if (settings.store.ignoreBots && e.message.author?.bot) return;
|
if (Settings.plugins.Moyai.ignoreBots && e.message.author?.bot) return;
|
||||||
if (!e.message.content) return;
|
if (!e.message.content) return;
|
||||||
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
|
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
|
||||||
|
|
||||||
@ -97,7 +76,7 @@ export default definePlugin({
|
|||||||
|
|
||||||
onReaction(e: IReactionAdd) {
|
onReaction(e: IReactionAdd) {
|
||||||
if (e.optimistic || e.type !== "MESSAGE_REACTION_ADD") return;
|
if (e.optimistic || e.type !== "MESSAGE_REACTION_ADD") return;
|
||||||
if (settings.store.ignoreBots && UserStore.getUser(e.userId)?.bot) return;
|
if (Settings.plugins.Moyai.ignoreBots && UserStore.getUser(e.userId)?.bot) return;
|
||||||
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
|
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
|
||||||
|
|
||||||
const name = e.emoji.name.toLowerCase();
|
const name = e.emoji.name.toLowerCase();
|
||||||
@ -124,6 +103,28 @@ export default definePlugin({
|
|||||||
FluxDispatcher.unsubscribe("MESSAGE_CREATE", this.onMessage);
|
FluxDispatcher.unsubscribe("MESSAGE_CREATE", this.onMessage);
|
||||||
FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD", this.onReaction);
|
FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD", this.onReaction);
|
||||||
FluxDispatcher.unsubscribe("VOICE_CHANNEL_EFFECT_SEND", this.onVoiceChannelEffect);
|
FluxDispatcher.unsubscribe("VOICE_CHANNEL_EFFECT_SEND", this.onVoiceChannelEffect);
|
||||||
|
},
|
||||||
|
|
||||||
|
options: {
|
||||||
|
volume: {
|
||||||
|
description: "Volume of the 🗿🗿🗿",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
markers: makeRange(0, 1, 0.1),
|
||||||
|
default: 0.5,
|
||||||
|
stickToMarkers: false,
|
||||||
|
},
|
||||||
|
triggerWhenUnfocused: {
|
||||||
|
description: "Trigger the 🗿 even when the window is unfocused",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: false,
|
||||||
|
},
|
||||||
|
ignoreBots: {
|
||||||
|
description: "Ignore bots",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -157,9 +158,9 @@ function getMoyaiCount(message: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function boom() {
|
function boom() {
|
||||||
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
|
if (!Settings.plugins.Moyai.triggerWhenUnfocused && !document.hasFocus()) return;
|
||||||
const audioElement = document.createElement("audio");
|
const audioElement = document.createElement("audio");
|
||||||
audioElement.src = MOYAI_URL;
|
audioElement.src = MOYAI_URL;
|
||||||
audioElement.volume = settings.store.volume;
|
audioElement.volume = Settings.plugins.Moyai.volume;
|
||||||
audioElement.play();
|
audioElement.play();
|
||||||
}
|
}
|
||||||
|
@ -37,19 +37,16 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...[
|
{
|
||||||
'displayName="MessageStore"',
|
find: "displayName=\"MessageStore\"",
|
||||||
'displayName="ReadStateStore"'
|
|
||||||
].map(find => ({
|
|
||||||
find,
|
|
||||||
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,
|
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=MESSAGE_CREATE:function\((\i)\){)/,
|
match: /(?<=MESSAGE_CREATE:function\((\w)\){var \w=\w\.channelId,\w=\w\.message,\w=\w\.isPushNotification,\w=\w\.\w\.getOrCreate\(\w\));/,
|
||||||
replace: (_, props) => `if($self.isBlocked(${props}.message))return;`
|
replace: ";if($self.isBlocked(n))return;"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}))
|
}
|
||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
ignoreBlockedMessages: {
|
ignoreBlockedMessages: {
|
||||||
|
@ -24,7 +24,7 @@ migratePluginSettings("NoDevtoolsWarning", "STFU");
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoDevtoolsWarning",
|
name: "NoDevtoolsWarning",
|
||||||
description: "Disables the 'HOLD UP' banner in the console. As a side effect, also prevents Discord from hiding your token, which prevents random logouts.",
|
description: "Disables the 'HOLD UP' banner in the console",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "setDevtoolsCallbacks",
|
find: "setDevtoolsCallbacks",
|
||||||
|
@ -16,13 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { migratePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
migratePluginSettings("NoF1", "No F1");
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoF1",
|
name: "No F1",
|
||||||
description: "Disables F1 help bind.",
|
description: "Disables F1 help bind.",
|
||||||
authors: [Devs.Cyn],
|
authors: [Devs.Cyn],
|
||||||
patches: [
|
patches: [
|
||||||
|
@ -16,15 +16,14 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { migratePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
migratePluginSettings("NoRPC", "No RPC");
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoRPC",
|
name: "No RPC",
|
||||||
description: "Disables Discord's RPC server.",
|
description: "Disables Discord's RPC server.",
|
||||||
authors: [Devs.Cyn],
|
authors: [Devs.Cyn],
|
||||||
|
target: "DESKTOP",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '.ensureModule("discord_rpc")',
|
find: '.ensureModule("discord_rpc")',
|
@ -27,12 +27,12 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: '("ApplicationStreamPreviewUploadManager")',
|
find: '("ApplicationStreamPreviewUploadManager")',
|
||||||
replacement: [
|
replacement: [
|
||||||
"\\i\\.default\\.makeChunkedRequest\\(",
|
".\\.default\\.makeChunkedRequest\\(",
|
||||||
"\\i\\.\\i\\.post\\({url:"
|
".{1,2}\\..\\.post\\({url:"
|
||||||
].map(match => ({
|
].map(match => ({
|
||||||
match: new RegExp(`(?=return\\[(\\d),${match}\\i\\.\\i\\.STREAM_PREVIEW.+?}\\)\\];)`),
|
match: new RegExp(`return\\[(?<code>\\d),${match}.\\..{1,3}\\.STREAM_PREVIEW.+?}\\)\\];`),
|
||||||
replace: (_, code) => `return[${code},Promise.resolve({body:"",status:204})];`
|
replace: 'return[$<code>,Promise.resolve({body:"",status:204})];'
|
||||||
}))
|
}))
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@ export default definePlugin({
|
|||||||
name: "NoSystemBadge",
|
name: "NoSystemBadge",
|
||||||
description: "Disables the taskbar and system tray unread count badge.",
|
description: "Disables the taskbar and system tray unread count badge.",
|
||||||
authors: [Devs.rushii],
|
authors: [Devs.rushii],
|
||||||
|
target: "DESKTOP",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "setSystemTrayApplications:function",
|
find: "setSystemTrayApplications:function",
|
@ -21,8 +21,8 @@ import definePlugin from "@utils/types";
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoTrack",
|
name: "NoTrack",
|
||||||
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting",
|
description: "Disable Discord's tracking and crash reporting",
|
||||||
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz],
|
authors: [Devs.Cyn],
|
||||||
required: true,
|
required: true,
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
@ -35,22 +35,9 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: "window.DiscordSentry=",
|
find: "window.DiscordSentry=",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /^.+$/,
|
match: /window\.DiscordSentry=function.+\}\(\)/,
|
||||||
replace: "()=>{}",
|
replace: "",
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".METRICS,",
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /this\._intervalId.+?12e4\)/,
|
|
||||||
replace: ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /(?<=increment=function\(\i\){)/,
|
|
||||||
replace: "return;"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -23,11 +23,11 @@ import { Settings } from "@api/settings";
|
|||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByCodeLazy, findStoreLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
|
import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
const SessionsStore = findStoreLazy("SessionsStore");
|
const SessionStore = findByPropsLazy("getActiveSession");
|
||||||
|
|
||||||
function Icon(path: string, viewBox = "0 0 24 24") {
|
function Icon(path: string, viewBox = "0 0 24 24") {
|
||||||
return ({ color, tooltip }: { color: string; tooltip: string; }) => (
|
return ({ color, tooltip }: { color: string; tooltip: string; }) => (
|
||||||
@ -70,7 +70,7 @@ const PlatformIndicator = ({ user, inline = false, marginLeft = "4px" }: { user:
|
|||||||
if (!user || user.bot) return null;
|
if (!user || user.bot) return null;
|
||||||
|
|
||||||
if (user.id === UserStore.getCurrentUser().id) {
|
if (user.id === UserStore.getCurrentUser().id) {
|
||||||
const sessions = SessionsStore.getSessions();
|
const sessions = SessionStore.getSessions();
|
||||||
if (typeof sessions !== "object") return null;
|
if (typeof sessions !== "object") return null;
|
||||||
const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => {
|
const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => {
|
||||||
if (a === b) return 0;
|
if (a === b) return 0;
|
||||||
@ -156,7 +156,7 @@ const indicatorLocations = {
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PlatformIndicators",
|
name: "PlatformIndicators",
|
||||||
description: "Adds platform indicators (Desktop, Mobile, Web...) to users",
|
description: "Adds platform indicators (Desktop, Mobile, Web...) to users",
|
||||||
authors: [Devs.kemo, Devs.TheSun, Devs.Nuckyz],
|
authors: [Devs.kemo, Devs.TheSun],
|
||||||
dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"],
|
dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"],
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
@ -185,55 +185,6 @@ export default definePlugin({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: ".Masks.STATUS_ONLINE_MOBILE",
|
|
||||||
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
// Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status
|
|
||||||
match: /(?<=return \i\.\i\.Masks\.STATUS_TYPING;)(.+?)(\i)\?(\i\.\i\.Masks\.STATUS_ONLINE_MOBILE):/,
|
|
||||||
replace: (_, rest, isMobile, mobileMask) => `if(${isMobile})return ${mobileMask};${rest}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status
|
|
||||||
match: /(switch\(\i\){case \i\.\i\.ONLINE:return )(\i)\?({.+?}):/,
|
|
||||||
replace: (_, rest, isMobile, component) => `if(${isMobile})return${component};${rest}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".AVATAR_STATUS_MOBILE_16;",
|
|
||||||
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
// Return the AVATAR_STATUS_MOBILE size mask if the user is on mobile, no matter the status
|
|
||||||
match: /\i===\i\.\i\.ONLINE&&(?=.{0,70}\.AVATAR_STATUS_MOBILE_16;)/,
|
|
||||||
replace: ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Fix sizes for mobile indicators which aren't online
|
|
||||||
match: /(?<=\(\i\.status,)(\i)(?=,(\i),\i\))/,
|
|
||||||
replace: (_, userStatus, isMobile) => `${isMobile}?"online":${userStatus}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Make isMobile true no matter the status
|
|
||||||
match: /(?<=\i&&!\i)&&\i===\i\.\i\.ONLINE/,
|
|
||||||
replace: ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "isMobileOnline=function",
|
|
||||||
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
|
|
||||||
replacement: {
|
|
||||||
// Make isMobileOnline return true no matter what is the user status
|
|
||||||
match: /(?<=\i\[\i\.\i\.MOBILE\])===\i\.\i\.ONLINE/,
|
|
||||||
replace: "!= null"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Object.entries(indicatorLocations).map(([key, value]) => {
|
Object.entries(indicatorLocations).map(([key, value]) => {
|
||||||
@ -245,12 +196,6 @@ export default definePlugin({
|
|||||||
default: true
|
default: true
|
||||||
}];
|
}];
|
||||||
})
|
})
|
||||||
),
|
)
|
||||||
colorMobileIndicator: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Whether to make the mobile indicator match the color of the user status.",
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -17,63 +17,39 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
import { classes } from "@utils/misc";
|
import { classes, useAwaiter } from "@utils/misc";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { UserStore } from "@webpack/common";
|
import { UserStore } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
import { awaitAndFormatPronouns } from "../pronoundbUtils";
|
import { fetchPronouns, formatPronouns } from "../pronoundbUtils";
|
||||||
|
import { PronounMapping } from "../types";
|
||||||
|
|
||||||
const styles: Record<string, string> = findByPropsLazy("timestampInline");
|
const styles: Record<string, string> = findByPropsLazy("timestampInline");
|
||||||
|
|
||||||
function shouldShow(message: Message): boolean {
|
export default function PronounsChatComponentWrapper({ message }: { message: Message; }) {
|
||||||
// Respect showInMessages
|
|
||||||
if (!Settings.plugins.PronounDB.showInMessages)
|
|
||||||
return false;
|
|
||||||
// Don't bother fetching bot or system users
|
// Don't bother fetching bot or system users
|
||||||
if (message.author.bot || message.author.system)
|
if (message.author.bot || message.author.system)
|
||||||
return false;
|
return null;
|
||||||
// Respect showSelf options
|
// Respect showSelf options
|
||||||
if (!Settings.plugins.PronounDB.showSelf && message.author.id === UserStore.getCurrentUser().id)
|
if (!Settings.plugins.PronounDB.showSelf && message.author.id === UserStore.getCurrentUser().id)
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PronounsChatComponentWrapper({ message }: { message: Message; }) {
|
|
||||||
if (!shouldShow(message))
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return <PronounsChatComponent message={message} />;
|
return <PronounsChatComponent message={message} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CompactPronounsChatComponentWrapper({ message }: { message: Message; }) {
|
|
||||||
if (!shouldShow(message))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return <CompactPronounsChatComponent message={message} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PronounsChatComponent({ message }: { message: Message; }) {
|
function PronounsChatComponent({ message }: { message: Message; }) {
|
||||||
const result = awaitAndFormatPronouns(message.author.id);
|
const [result, , isPending] = useAwaiter(() => fetchPronouns(message.author.id), {
|
||||||
if (result != null) {
|
fallbackValue: null,
|
||||||
|
onError: e => console.error("Fetching pronouns failed: ", e)
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return a span with the pronouns
|
||||||
|
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={classes(styles.timestampInline, styles.timestamp)}
|
className={classes(styles.timestampInline, styles.timestamp)}
|
||||||
>• {result}</span>
|
>• {formatPronouns(result)}</span>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CompactPronounsChatComponent({ message }: { message: Message; }) {
|
|
||||||
const result = awaitAndFormatPronouns(message.author.id);
|
|
||||||
if (result != null) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={classes(styles.timestampInline, styles.timestamp, "vc-pronoundb-compact")}
|
|
||||||
>• {result}</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,19 +17,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
|
import { useAwaiter } from "@utils/misc";
|
||||||
import { UserStore } from "@webpack/common";
|
import { UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { awaitAndFormatPronouns } from "../pronoundbUtils";
|
import { fetchPronouns, formatPronouns } from "../pronoundbUtils";
|
||||||
import { UserProfilePronounsProps, UserProfileProps } from "../types";
|
import { PronounMapping, UserProfilePronounsProps, UserProfileProps } from "../types";
|
||||||
|
|
||||||
export default function PronounsProfileWrapper(PronounsComponent: React.ElementType<UserProfilePronounsProps>, props: UserProfilePronounsProps, profileProps: UserProfileProps) {
|
export default function PronounsProfileWrapper(PronounsComponent: React.ElementType<UserProfilePronounsProps>, props: UserProfilePronounsProps, profileProps: UserProfileProps) {
|
||||||
const user = UserStore.getUser(profileProps.userId) ?? {};
|
const user = UserStore.getUser(profileProps.userId) ?? {};
|
||||||
// Respect showInProfile
|
|
||||||
if (!Settings.plugins.PronounDB.showInProfile)
|
|
||||||
return null;
|
|
||||||
// Don't bother fetching bot or system users
|
// Don't bother fetching bot or system users
|
||||||
if (user.bot || user.system)
|
if (user.bot || user.system) return null;
|
||||||
return null;
|
|
||||||
// Respect showSelf options
|
// Respect showSelf options
|
||||||
if (!Settings.plugins.PronounDB.showSelf && user.id === UserStore.getCurrentUser().id)
|
if (!Settings.plugins.PronounDB.showSelf && user.id === UserStore.getCurrentUser().id)
|
||||||
return null;
|
return null;
|
||||||
@ -48,12 +45,15 @@ function ProfilePronouns(
|
|||||||
leProps: UserProfilePronounsProps;
|
leProps: UserProfilePronounsProps;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const result = awaitAndFormatPronouns(userId);
|
const [result, , isPending] = useAwaiter(() => fetchPronouns(userId), {
|
||||||
|
fallbackValue: null,
|
||||||
|
onError: e => console.error("Fetching pronouns failed: ", e),
|
||||||
|
});
|
||||||
|
|
||||||
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then render
|
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then render
|
||||||
if (result != null) {
|
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) {
|
||||||
// First child is the header, second is a div with the actual text
|
// First child is the header, second is a div with the actual text
|
||||||
leProps.currentPronouns ||= result;
|
leProps.currentPronouns ||= formatPronouns(result);
|
||||||
return <Component {...leProps} />;
|
return <Component {...leProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,13 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./styles.css";
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
import PronounsAboutComponent from "./components/PronounsAboutComponent";
|
import PronounsAboutComponent from "./components/PronounsAboutComponent";
|
||||||
import { CompactPronounsChatComponentWrapper, PronounsChatComponentWrapper } from "./components/PronounsChatComponent";
|
import PronounsChatComponent from "./components/PronounsChatComponent";
|
||||||
import PronounsProfileWrapper from "./components/PronounsProfileWrapper";
|
import PronounsProfileWrapper from "./components/PronounsProfileWrapper";
|
||||||
|
|
||||||
export enum PronounsFormat {
|
export enum PronounsFormat {
|
||||||
@ -32,39 +30,31 @@ export enum PronounsFormat {
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PronounDB",
|
name: "PronounDB",
|
||||||
authors: [Devs.Tyman, Devs.TheKodeToad],
|
authors: [Devs.Tyman],
|
||||||
description: "Adds pronouns to user messages using pronoundb",
|
description: "Adds pronouns to user messages using pronoundb",
|
||||||
patches: [
|
patches: [
|
||||||
// Add next to username (compact mode)
|
// Patch the chat timestamp element
|
||||||
{
|
{
|
||||||
find: "showCommunicationDisabledStyles",
|
find: "showCommunicationDisabledStyles",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /("span",{id:\i,className:\i,children:\i}\))/,
|
match: /(?<=return\s*\(0,\w{1,3}\.jsxs?\)\(.+!\w{1,3}&&)(\(0,\w{1,3}.jsxs?\)\(.+?\{.+?\}\))/,
|
||||||
replace: "$1, $self.CompactPronounsChatComponentWrapper(e)"
|
replace: "[$1, $self.PronounsChatComponent(e)]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Patch the chat timestamp element (normal mode)
|
// Hijack the discord pronouns section (hidden without experiment) and add a wrapper around the text section
|
||||||
{
|
|
||||||
find: "showCommunicationDisabledStyles",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=return\s*\(0,\i\.jsxs?\)\(.+!\i&&)(\(0,\i.jsxs?\)\(.+?\{.+?\}\))/,
|
|
||||||
replace: "[$1, $self.PronounsChatComponentWrapper(e)]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Hijack the discord pronouns section and add a wrapper around the text section
|
|
||||||
{
|
{
|
||||||
find: ".Messages.BOT_PROFILE_SLASH_COMMANDS",
|
find: ".Messages.BOT_PROFILE_SLASH_COMMANDS",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\(0,.\.jsx\)\((?<PronounComponent>\i\..),(?<pronounProps>{currentPronouns.+?:(?<fullProps>\i)\.pronouns.+?})\)/,
|
match: /\(0,.\.jsx\)\((?<PronounComponent>.{1,2}\..),(?<pronounProps>{currentPronouns.+?:(?<fullProps>.{1,2})\.pronouns.+?})\)/,
|
||||||
replace: "$<fullProps>&&$self.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)"
|
replace: "$<fullProps>&&$self.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Force enable pronouns component ignoring the experiment value
|
// Make pronouns experiment be enabled by default
|
||||||
{
|
{
|
||||||
find: ".Messages.USER_POPOUT_PRONOUNS",
|
find: "2022-01_pronouns",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\.showPronouns/,
|
match: "!1", // false
|
||||||
replace: ".showPronouns||true"
|
replace: "!0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -89,21 +79,10 @@ export default definePlugin({
|
|||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Enable or disable showing pronouns for the current user",
|
description: "Enable or disable showing pronouns for the current user",
|
||||||
default: true
|
default: true
|
||||||
},
|
|
||||||
showInMessages: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Show in messages",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showInProfile: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Show in profile",
|
|
||||||
default: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
settingsAboutComponent: PronounsAboutComponent,
|
settingsAboutComponent: PronounsAboutComponent,
|
||||||
// Re-export the components on the plugin object so it is easily accessible in patches
|
// Re-export the components on the plugin object so it is easily accessible in patches
|
||||||
PronounsChatComponentWrapper,
|
PronounsChatComponent,
|
||||||
CompactPronounsChatComponentWrapper,
|
|
||||||
PronounsProfileWrapper
|
PronounsProfileWrapper
|
||||||
});
|
});
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
import { Settings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
import { VENCORD_USER_AGENT } from "@utils/constants";
|
import { VENCORD_USER_AGENT } from "@utils/constants";
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { useAwaiter } from "@utils/misc";
|
|
||||||
|
|
||||||
import { PronounsFormat } from ".";
|
import { PronounsFormat } from ".";
|
||||||
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||||
@ -40,19 +39,6 @@ const bulkFetch = debounce(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function awaitAndFormatPronouns(id: string): string | null {
|
|
||||||
const [result, , isPending] = useAwaiter(() => fetchPronouns(id), {
|
|
||||||
fallbackValue: null,
|
|
||||||
onError: e => console.error("Fetching pronouns failed: ", e)
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return the mappings
|
|
||||||
if (!isPending && result && result !== "unspecified" && PronounMapping[result])
|
|
||||||
return formatPronouns(result);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches the pronouns for one id, returning a promise that resolves if it was cached, or once the request is completed
|
// Fetches the pronouns for one id, returning a promise that resolves if it was cached, or once the request is completed
|
||||||
export function fetchPronouns(id: string): Promise<PronounCode> {
|
export function fetchPronouns(id: string): Promise<PronounCode> {
|
||||||
return new Promise(res => {
|
return new Promise(res => {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
.vc-pronoundb-compact {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class*="compact"] .vc-pronoundb-compact {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: -2px;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
250
src/plugins/relationshipNotifier.ts
Normal file
250
src/plugins/relationshipNotifier.ts
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
/*
|
||||||
|
* 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 { showNotification } from "@api/Notifications";
|
||||||
|
import { definePluginSettings } from "@api/settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { FluxDispatcher, UserUtils } from "@webpack/common";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
|
enum RelationshipType {
|
||||||
|
NONE = 0,
|
||||||
|
FRIEND = 1,
|
||||||
|
BLOCKED = 2,
|
||||||
|
PENDING_INCOMING = 3,
|
||||||
|
PENDING_OUTGOING = 4,
|
||||||
|
IMPLICIT = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelationshipPayload {
|
||||||
|
type: "RELATIONSHIP_ADD" | "RELATIONSHIP_REMOVE" | "RELATIONSHIP_UPDATE";
|
||||||
|
relationship: {
|
||||||
|
id: string;
|
||||||
|
type: RelationshipType;
|
||||||
|
since?: Date;
|
||||||
|
nickname?: string;
|
||||||
|
user?: User;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
friend: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Show a notification when a friend is added or removed",
|
||||||
|
options: [{
|
||||||
|
label: "Friend added and removed",
|
||||||
|
value: "ALL",
|
||||||
|
default: true,
|
||||||
|
}, {
|
||||||
|
label: "Only when added",
|
||||||
|
value: "CREATE",
|
||||||
|
}, {
|
||||||
|
label: "Only when removed",
|
||||||
|
value: "REMOVE",
|
||||||
|
}, {
|
||||||
|
label: "No notifications",
|
||||||
|
value: "NONE",
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
outgoingRequest: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Show a notification when you send or cancel a friend request",
|
||||||
|
options: [{
|
||||||
|
label: "Request sent and cancelled",
|
||||||
|
value: "ALL",
|
||||||
|
default: true,
|
||||||
|
}, {
|
||||||
|
label: "Only when sent",
|
||||||
|
value: "CREATE",
|
||||||
|
}, {
|
||||||
|
label: "Only when cancelled",
|
||||||
|
value: "REMOVE",
|
||||||
|
}, {
|
||||||
|
label: "No notifications",
|
||||||
|
value: "NONE",
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
incomingRequest: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Show a notification when an incoming request is received or cancelled",
|
||||||
|
options: [{
|
||||||
|
label: "Request received and cancelled",
|
||||||
|
value: "ALL",
|
||||||
|
default: true,
|
||||||
|
}, {
|
||||||
|
label: "Only when received",
|
||||||
|
value: "CREATE",
|
||||||
|
}, {
|
||||||
|
label: "Only when cancelled",
|
||||||
|
value: "REMOVE",
|
||||||
|
}, {
|
||||||
|
label: "No notifications",
|
||||||
|
value: "NONE",
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Show a notification when you block or unblock a user",
|
||||||
|
options: [{
|
||||||
|
label: "Blocking and unblocking",
|
||||||
|
value: "ALL",
|
||||||
|
default: true,
|
||||||
|
}, {
|
||||||
|
label: "Only when blocking",
|
||||||
|
value: "CREATE",
|
||||||
|
}, {
|
||||||
|
label: "Only when unblocking",
|
||||||
|
value: "REMOVE",
|
||||||
|
}, {
|
||||||
|
label: "No notifications",
|
||||||
|
value: "NONE",
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "RelationshipNotifier",
|
||||||
|
authors: [Devs.Megu],
|
||||||
|
description: "Receive notifications for friend requests, removals, blocks, etc.",
|
||||||
|
settings,
|
||||||
|
|
||||||
|
start() {
|
||||||
|
FluxDispatcher.subscribe("RELATIONSHIP_ADD", onRelationshipUpdate);
|
||||||
|
FluxDispatcher.subscribe("RELATIONSHIP_UPDATE", onRelationshipUpdate);
|
||||||
|
FluxDispatcher.subscribe("RELATIONSHIP_REMOVE", onRelationshipRemove);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
FluxDispatcher.unsubscribe("RELATIONSHIP_ADD", onRelationshipUpdate);
|
||||||
|
FluxDispatcher.unsubscribe("RELATIONSHIP_UPDATE", onRelationshipUpdate);
|
||||||
|
FluxDispatcher.unsubscribe("RELATIONSHIP_REMOVE", onRelationshipRemove);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onRelationshipUpdate({ relationship }: RelationshipPayload) {
|
||||||
|
if (!relationship.id) return;
|
||||||
|
const user = await UserUtils.fetchUser(relationship.id);
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "USER_PROFILE_MODAL_OPEN",
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (relationship.type) {
|
||||||
|
case RelationshipType.FRIEND: {
|
||||||
|
if (!["ALL", "CREATE"].includes(settings.store.friend)) break;
|
||||||
|
showNotification({
|
||||||
|
title: "Friend Added",
|
||||||
|
body: `${user.username} is now your friend.`,
|
||||||
|
icon: user.getAvatarURL(),
|
||||||
|
onClick,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RelationshipType.PENDING_INCOMING: {
|
||||||
|
if (!["ALL", "CREATE"].includes(settings.store.incomingRequest)) break;
|
||||||
|
showNotification({
|
||||||
|
title: "Friend Request Received",
|
||||||
|
body: `${user.username} sent you a friend request.`,
|
||||||
|
icon: user.getAvatarURL(),
|
||||||
|
onClick,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RelationshipType.PENDING_OUTGOING: {
|
||||||
|
if (!["ALL", "CREATE"].includes(settings.store.outgoingRequest)) break;
|
||||||
|
showNotification({
|
||||||
|
title: "Friend Request Sent",
|
||||||
|
body: `You sent a friend request to ${user.username}`,
|
||||||
|
icon: user.getAvatarURL(),
|
||||||
|
onClick
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RelationshipType.BLOCKED: {
|
||||||
|
if (!["ALL", "CREATE"].includes(settings.store.block)) break;
|
||||||
|
showNotification({
|
||||||
|
title: "User Blocked",
|
||||||
|
body: `You just blocked ${user.username}`,
|
||||||
|
icon: user.getAvatarURL(),
|
||||||
|
onClick
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRelationshipRemove({ relationship }: RelationshipPayload) {
|
||||||
|
if (!relationship.id) return;
|
||||||
|
const user = await UserUtils.fetchUser(relationship.id);
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "USER_PROFILE_MODAL_OPEN",
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (relationship.type) {
|
||||||
|
case RelationshipType.FRIEND: {
|
||||||
|
if (!["ALL", "REMOVE"].includes(settings.store.friend)) break;
|
||||||
|
showNotification({
|
||||||
|
title: "Friend Removed",
|
||||||
|
body: `${user.username} is no longer on your friends list.`,
|
||||||
|
icon: user.getAvatarURL(),
|
||||||
|
onClick,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RelationshipType.PENDING_INCOMING: {
|
||||||
|
if (!["ALL", "REMOVE"].includes(settings.store.incomingRequest)) break;
|
||||||
|
showNotification({
|
||||||
|
title: "Friend Request Cancelled",
|
||||||
|
body: `${user.username} cancelled their friend request.`,
|
||||||
|
icon: user.getAvatarURL(),
|
||||||
|
onClick,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RelationshipType.PENDING_OUTGOING: {
|
||||||
|
if (!["ALL", "REMOVE"].includes(settings.store.outgoingRequest)) break;
|
||||||
|
showNotification({
|
||||||
|
title: "Friend Request Cancelled",
|
||||||
|
body: `You cancelled your friend request to ${user.username}`,
|
||||||
|
icon: user.getAvatarURL(),
|
||||||
|
onClick
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RelationshipType.BLOCKED: {
|
||||||
|
if (!["ALL", "REMOVE"].includes(settings.store.block)) break;
|
||||||
|
showNotification({
|
||||||
|
title: "User Unblocked",
|
||||||
|
body: `You just unblocked ${user.username}`,
|
||||||
|
icon: user.getAvatarURL(),
|
||||||
|
onClick
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { FluxEvents } from "@webpack/types";
|
|
||||||
|
|
||||||
import { onChannelDelete, onGuildDelete, onRelationshipRemove } from "./functions";
|
|
||||||
import { syncFriends, syncGroups, syncGuilds } from "./utils";
|
|
||||||
|
|
||||||
export const FluxHandlers: Partial<Record<FluxEvents, Array<(data: any) => void>>> = {
|
|
||||||
GUILD_CREATE: [syncGuilds],
|
|
||||||
GUILD_DELETE: [onGuildDelete],
|
|
||||||
CHANNEL_CREATE: [syncGroups],
|
|
||||||
CHANNEL_DELETE: [onChannelDelete],
|
|
||||||
RELATIONSHIP_ADD: [syncFriends],
|
|
||||||
RELATIONSHIP_UPDATE: [syncFriends],
|
|
||||||
RELATIONSHIP_REMOVE: [syncFriends, onRelationshipRemove]
|
|
||||||
};
|
|
||||||
|
|
||||||
export function forEachEvent(fn: (event: FluxEvents, handler: (data: any) => void) => void) {
|
|
||||||
for (const event in FluxHandlers) {
|
|
||||||
for (const cb of FluxHandlers[event]) {
|
|
||||||
fn(event as FluxEvents, cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { UserUtils } from "@webpack/common";
|
|
||||||
|
|
||||||
import settings from "./settings";
|
|
||||||
import { ChannelDelete, ChannelType, GuildDelete, RelationshipRemove, RelationshipType } from "./types";
|
|
||||||
import { deleteGroup, deleteGuild, getGroup, getGuild, notify } from "./utils";
|
|
||||||
|
|
||||||
let manuallyRemovedFriend: string | undefined;
|
|
||||||
let manuallyRemovedGuild: string | undefined;
|
|
||||||
let manuallyRemovedGroup: string | undefined;
|
|
||||||
|
|
||||||
export const removeFriend = (id: string) => manuallyRemovedFriend = id;
|
|
||||||
export const removeGuild = (id: string) => manuallyRemovedGuild = id;
|
|
||||||
export const removeGroup = (id: string) => manuallyRemovedGroup = id;
|
|
||||||
|
|
||||||
export async function onRelationshipRemove({ relationship: { type, id } }: RelationshipRemove) {
|
|
||||||
if (manuallyRemovedFriend === id) {
|
|
||||||
manuallyRemovedFriend = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await UserUtils.fetchUser(id)
|
|
||||||
.catch(() => null);
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case RelationshipType.FRIEND:
|
|
||||||
if (settings.store.friends)
|
|
||||||
notify(`${user.tag} removed you as a friend.`, user.getAvatarURL(undefined, undefined, false));
|
|
||||||
break;
|
|
||||||
case RelationshipType.FRIEND_REQUEST:
|
|
||||||
if (settings.store.friendRequestCancels)
|
|
||||||
notify(`A friend request from ${user.tag} has been removed.`, user.getAvatarURL(undefined, undefined, false));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onGuildDelete({ guild: { id, unavailable } }: GuildDelete) {
|
|
||||||
if (!settings.store.servers) return;
|
|
||||||
if (unavailable) return;
|
|
||||||
|
|
||||||
if (manuallyRemovedGuild === id) {
|
|
||||||
deleteGuild(id);
|
|
||||||
manuallyRemovedGuild = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const guild = getGuild(id);
|
|
||||||
if (guild) {
|
|
||||||
deleteGuild(id);
|
|
||||||
notify(`You were removed from the server ${guild.name}.`, guild.iconURL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onChannelDelete({ channel: { id, type } }: ChannelDelete) {
|
|
||||||
if (!settings.store.groups) return;
|
|
||||||
if (type !== ChannelType.GROUP_DM) return;
|
|
||||||
|
|
||||||
if (manuallyRemovedGroup === id) {
|
|
||||||
deleteGroup(id);
|
|
||||||
manuallyRemovedGroup = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = getGroup(id);
|
|
||||||
if (group) {
|
|
||||||
deleteGroup(id);
|
|
||||||
notify(`You were removed from the group ${group.name}.`, group.iconURL);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { FluxDispatcher } from "@webpack/common";
|
|
||||||
|
|
||||||
import { forEachEvent } from "./events";
|
|
||||||
import { removeFriend, removeGroup, removeGuild } from "./functions";
|
|
||||||
import settings from "./settings";
|
|
||||||
import { syncAndRunChecks } from "./utils";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "RelationshipNotifier",
|
|
||||||
description: "Notifies you when a friend, group chat, or server removes you.",
|
|
||||||
authors: [Devs.nick],
|
|
||||||
settings,
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "removeRelationship:function(",
|
|
||||||
replacement: {
|
|
||||||
match: /(removeRelationship:function\((\i),\i,\i\){)/,
|
|
||||||
replace: "$1$self.removeFriend($2);"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "leaveGuild:function(",
|
|
||||||
replacement: {
|
|
||||||
match: /(leaveGuild:function\((\i)\){)/,
|
|
||||||
replace: "$1$self.removeGuild($2);"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "closePrivateChannel:function(",
|
|
||||||
replacement: {
|
|
||||||
match: /(closePrivateChannel:function\((\i)\){)/,
|
|
||||||
replace: "$1$self.removeGroup($2);"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
setTimeout(() => {
|
|
||||||
syncAndRunChecks();
|
|
||||||
}, 5000);
|
|
||||||
forEachEvent((ev, cb) => FluxDispatcher.subscribe(ev, cb));
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
forEachEvent((ev, cb) => FluxDispatcher.unsubscribe(ev, cb));
|
|
||||||
},
|
|
||||||
|
|
||||||
removeFriend,
|
|
||||||
removeGroup,
|
|
||||||
removeGuild
|
|
||||||
});
|
|
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { OptionType } from "@utils/types";
|
|
||||||
|
|
||||||
export default definePluginSettings({
|
|
||||||
notices: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Also show a notice at the top of your screen when removed (use this if you don't want to miss any notifications).",
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
offlineRemovals: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Notify you when starting discord if you were removed while offline.",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
friends: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Notify when a friend removes you",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
friendRequestCancels: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Notify when a friend request is cancelled",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
servers: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Notify when removed from a server",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
groups: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Notify when removed from a group chat",
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { Channel } from "discord-types/general";
|
|
||||||
|
|
||||||
export interface ChannelDelete {
|
|
||||||
type: "CHANNEL_DELETE";
|
|
||||||
channel: Channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GuildDelete {
|
|
||||||
type: "GUILD_DELETE";
|
|
||||||
guild: {
|
|
||||||
id: string;
|
|
||||||
unavailable?: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RelationshipRemove {
|
|
||||||
type: "RELATIONSHIP_REMOVE";
|
|
||||||
relationship: {
|
|
||||||
id: string;
|
|
||||||
nickname: string;
|
|
||||||
type: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SimpleGroupChannel {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
iconURL?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SimpleGuild {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
iconURL?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum ChannelType {
|
|
||||||
GROUP_DM = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum RelationshipType {
|
|
||||||
FRIEND = 1,
|
|
||||||
FRIEND_REQUEST = 3,
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { DataStore, Notices } from "@api/index";
|
|
||||||
import { showNotification } from "@api/Notifications";
|
|
||||||
import { ChannelStore, GuildStore, RelationshipStore, UserUtils } from "@webpack/common";
|
|
||||||
|
|
||||||
import settings from "./settings";
|
|
||||||
import { ChannelType, RelationshipType, SimpleGroupChannel, SimpleGuild } from "./types";
|
|
||||||
|
|
||||||
const guilds = new Map<string, SimpleGuild>();
|
|
||||||
const groups = new Map<string, SimpleGroupChannel>();
|
|
||||||
const friends = {
|
|
||||||
friends: [] as string[],
|
|
||||||
requests: [] as string[]
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function syncAndRunChecks() {
|
|
||||||
const [oldGuilds, oldGroups, oldFriends] = await DataStore.getMany([
|
|
||||||
"relationship-notifier-guilds",
|
|
||||||
"relationship-notifier-groups",
|
|
||||||
"relationship-notifier-friends"
|
|
||||||
]) as [Map<string, SimpleGuild> | undefined, Map<string, SimpleGroupChannel> | undefined, Record<"friends" | "requests", string[]> | undefined];
|
|
||||||
|
|
||||||
await Promise.all([syncGuilds(), syncGroups(), syncFriends()]);
|
|
||||||
|
|
||||||
if (settings.store.offlineRemovals) {
|
|
||||||
if (settings.store.groups && oldGroups?.size) {
|
|
||||||
for (const [id, group] of oldGroups) {
|
|
||||||
if (!groups.has(id))
|
|
||||||
notify(`You are no longer in the group ${group.name}.`, group.iconURL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.store.servers && oldGuilds?.size) {
|
|
||||||
for (const [id, guild] of oldGuilds) {
|
|
||||||
if (!guilds.has(id))
|
|
||||||
notify(`You are no longer in the server ${guild.name}.`, guild.iconURL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.store.friends && oldFriends?.friends.length) {
|
|
||||||
for (const id of oldFriends.friends) {
|
|
||||||
if (friends.friends.includes(id)) continue;
|
|
||||||
|
|
||||||
const user = await UserUtils.fetchUser(id).catch(() => void 0);
|
|
||||||
if (user)
|
|
||||||
notify(`You are no longer friends with ${user.tag}.`, user.getAvatarURL(undefined, undefined, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.store.friendRequestCancels && oldFriends?.requests?.length) {
|
|
||||||
for (const id of oldFriends.requests) {
|
|
||||||
if (friends.requests.includes(id)) continue;
|
|
||||||
|
|
||||||
const user = await UserUtils.fetchUser(id).catch(() => void 0);
|
|
||||||
if (user)
|
|
||||||
notify(`Friend request from ${user.tag} has been revoked.`, user.getAvatarURL(undefined, undefined, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notify(text: string, icon?: string) {
|
|
||||||
if (settings.store.notices)
|
|
||||||
Notices.showNotice(text, "OK", () => Notices.popNotice());
|
|
||||||
|
|
||||||
showNotification({
|
|
||||||
title: "Relationship Notifier",
|
|
||||||
body: text,
|
|
||||||
icon
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGuild(id: string) {
|
|
||||||
return guilds.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteGuild(id: string) {
|
|
||||||
guilds.delete(id);
|
|
||||||
syncGuilds();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncGuilds() {
|
|
||||||
for (const [id, { name, icon }] of Object.entries(GuildStore.getGuilds())) {
|
|
||||||
guilds.set(id, {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
iconURL: icon && `https://cdn.discordapp.com/icons/${id}/${icon}.png`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await DataStore.set("relationship-notifier-guilds", guilds);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGroup(id: string) {
|
|
||||||
return groups.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteGroup(id: string) {
|
|
||||||
groups.delete(id);
|
|
||||||
syncGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncGroups() {
|
|
||||||
for (const { type, id, name, rawRecipients, icon } of ChannelStore.getSortedPrivateChannels()) {
|
|
||||||
if (type === ChannelType.GROUP_DM)
|
|
||||||
groups.set(id, {
|
|
||||||
id,
|
|
||||||
name: name || rawRecipients.map(r => r.username).join(", "),
|
|
||||||
iconURL: icon && `https://cdn.discordapp.com/channel-icons/${id}/${icon}.png`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await DataStore.set("relationship-notifier-groups", groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncFriends() {
|
|
||||||
friends.friends = [];
|
|
||||||
friends.requests = [];
|
|
||||||
|
|
||||||
const relationShips = RelationshipStore.getRelationships();
|
|
||||||
for (const id in relationShips) {
|
|
||||||
switch (relationShips[id]) {
|
|
||||||
case RelationshipType.FRIEND:
|
|
||||||
friends.friends.push(id);
|
|
||||||
break;
|
|
||||||
case RelationshipType.FRIEND_REQUEST:
|
|
||||||
friends.requests.push(id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await DataStore.set("relationship-notifier-friends", friends);
|
|
||||||
}
|
|
@ -30,10 +30,10 @@ export default definePlugin({
|
|||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".removeObscurity=function",
|
find: ".revealSpoiler=function",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\.removeObscurity=function\((\i)\){)/,
|
match: /\.revealSpoiler=function\((.{1,2})\){/,
|
||||||
replace: (_, event) => `$self.reveal(${event});`
|
replace: ".revealSpoiler=function($1){$self.reveal($1);"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Menu } from "@webpack/common";
|
import { Menu } from "@webpack/common";
|
||||||
@ -30,21 +29,39 @@ const Engines = {
|
|||||||
ImgOps: "https://imgops.com/start?url="
|
ImgOps: "https://imgops.com/start?url="
|
||||||
};
|
};
|
||||||
|
|
||||||
function search(src: string, engine: string) {
|
export default definePlugin({
|
||||||
open(engine + encodeURIComponent(src), "_blank");
|
name: "ReverseImageSearch",
|
||||||
}
|
description: "Adds ImageSearch to image context menus",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
dependencies: ["MenuItemDeobfuscatorAPI"],
|
||||||
|
patches: [{
|
||||||
|
find: "open-native-link",
|
||||||
|
replacement: {
|
||||||
|
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
||||||
|
replace: (m, src) =>
|
||||||
|
`${m},Vencord.Plugins.plugins.ReverseImageSearch.makeMenu(${src}, arguments[2])`
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
// pass the target to the open link menu so we can check if it's an image
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
|
||||||
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
makeMenu(src: string, target: HTMLElement) {
|
||||||
if (!props) return;
|
if (target && !(target instanceof HTMLImageElement) && target.attributes["data-role"]?.value !== "img")
|
||||||
const { reverseImageSearchType, itemHref, itemSrc } = props;
|
return null;
|
||||||
|
|
||||||
if (!reverseImageSearchType || reverseImageSearchType !== "img") return;
|
return (
|
||||||
|
|
||||||
const src = itemHref ?? itemSrc;
|
|
||||||
|
|
||||||
const group = findGroupChildrenByChildId("copy-link", children);
|
|
||||||
if (group && !group.some(child => child?.props?.id === "search-image")) {
|
|
||||||
group.push((
|
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
label="Search Image"
|
label="Search Image"
|
||||||
key="search-image"
|
key="search-image"
|
||||||
@ -57,7 +74,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =>
|
|||||||
key={key}
|
key={key}
|
||||||
id={key}
|
id={key}
|
||||||
label={engine}
|
label={engine}
|
||||||
action={() => search(src, Engines[engine])}
|
action={() => this.search(src, Engines[engine])}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -65,33 +82,14 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =>
|
|||||||
key="search-image-all"
|
key="search-image-all"
|
||||||
id="search-image-all"
|
id="search-image-all"
|
||||||
label="All"
|
label="All"
|
||||||
action={() => Object.values(Engines).forEach(e => search(src, e))}
|
action={() => Object.values(Engines).forEach(e => this.search(src, e))}
|
||||||
/>
|
/>
|
||||||
</Menu.MenuItem>
|
</Menu.MenuItem>
|
||||||
));
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "ReverseImageSearch",
|
|
||||||
description: "Adds ImageSearch to image context menus",
|
|
||||||
authors: [Devs.Ven, Devs.Nuckyz],
|
|
||||||
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
|
|
||||||
replacement: {
|
|
||||||
match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/,
|
|
||||||
replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
start() {
|
|
||||||
addContextMenuPatch("message", imageContextMenuPatch);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
// openUrl is a mangled export, so just match it in the module and pass it
|
||||||
removeContextMenuPatch("message", imageContextMenuPatch);
|
search(src: string, engine: string) {
|
||||||
|
open(engine + encodeURIComponent(src), "_blank");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
95
src/plugins/reviewDB/Utils/ReviewDBAPI.ts
Normal file
95
src/plugins/reviewDB/Utils/ReviewDBAPI.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Review } from "../entities/Review";
|
||||||
|
import { authorize, showToast } from "./Utils";
|
||||||
|
|
||||||
|
const API_URL = "https://manti.vendicated.dev";
|
||||||
|
|
||||||
|
const getToken = () => Settings.plugins.ReviewDB.token;
|
||||||
|
|
||||||
|
enum Response {
|
||||||
|
"Added your review" = 0,
|
||||||
|
"Updated your review" = 1,
|
||||||
|
"Error" = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReviews(id: string): Promise<Review[]> {
|
||||||
|
const res = await fetch(API_URL + "/getUserReviews?snowflakeFormat=string&discordid=" + id);
|
||||||
|
return await res.json() as Review[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addReview(review: any): Promise<Response> {
|
||||||
|
review.token = getToken();
|
||||||
|
|
||||||
|
if (!review.token) {
|
||||||
|
showToast("Please authorize to add a review.");
|
||||||
|
authorize();
|
||||||
|
return Response.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(API_URL + "/addUserReview", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(review),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(res => {
|
||||||
|
showToast(res);
|
||||||
|
return Response[res] ?? Response.Error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteReview(id: number): Promise<any> {
|
||||||
|
return fetch(API_URL + "/deleteReview", {
|
||||||
|
method: "POST",
|
||||||
|
headers: new Headers({
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
}),
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: getToken(),
|
||||||
|
reviewid: id
|
||||||
|
})
|
||||||
|
}).then(r => r.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reportReview(id: number) {
|
||||||
|
const res = await fetch(API_URL + "/reportReview", {
|
||||||
|
method: "POST",
|
||||||
|
headers: new Headers({
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
}),
|
||||||
|
body: JSON.stringify({
|
||||||
|
reviewid: id,
|
||||||
|
token: getToken()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
showToast(await res.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLastReviewID(id: string): Promise<number> {
|
||||||
|
return fetch(API_URL + "/getLastReviewID?discordid=" + id)
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(Number);
|
||||||
|
}
|
95
src/plugins/reviewDB/Utils/Utils.tsx
Normal file
95
src/plugins/reviewDB/Utils/Utils.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Devs } from "@utils/constants";
|
||||||
|
import Logger from "@utils/Logger";
|
||||||
|
import { openModal } from "@utils/modal";
|
||||||
|
import { findByProps } from "@webpack";
|
||||||
|
import { FluxDispatcher, React, SelectedChannelStore, Toasts, UserUtils } from "@webpack/common";
|
||||||
|
|
||||||
|
import { Review } from "../entities/Review";
|
||||||
|
|
||||||
|
export async function openUserProfileModal(userId: string) {
|
||||||
|
await UserUtils.fetchUser(userId);
|
||||||
|
|
||||||
|
await FluxDispatcher.dispatch({
|
||||||
|
type: "USER_PROFILE_MODAL_OPEN",
|
||||||
|
userId,
|
||||||
|
channelId: SelectedChannelStore.getChannelId(),
|
||||||
|
analyticsLocation: "Explosive Hotel"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authorize(callback?: any) {
|
||||||
|
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
|
||||||
|
|
||||||
|
openModal((props: any) =>
|
||||||
|
<OAuth2AuthorizeModal
|
||||||
|
{...props}
|
||||||
|
scopes={["identify"]}
|
||||||
|
responseType="code"
|
||||||
|
redirectUri="https://manti.vendicated.dev/URauth"
|
||||||
|
permissions={0n}
|
||||||
|
clientId="915703782174752809"
|
||||||
|
cancelCompletesFlow={false}
|
||||||
|
callback={async (u: string) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(u);
|
||||||
|
url.searchParams.append("returnType", "json");
|
||||||
|
url.searchParams.append("clientMod", "vencord");
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: new Headers({ Accept: "application/json" })
|
||||||
|
});
|
||||||
|
const { token, status } = await res.json();
|
||||||
|
if (status === 0) {
|
||||||
|
Settings.plugins.ReviewDB.token = token;
|
||||||
|
showToast("Successfully logged in!");
|
||||||
|
callback?.();
|
||||||
|
} else if (res.status === 1) {
|
||||||
|
showToast("An Error occurred while logging in.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
new Logger("ReviewDB").error("Failed to authorise", e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showToast(text: string) {
|
||||||
|
Toasts.show({
|
||||||
|
type: Toasts.Type.MESSAGE,
|
||||||
|
message: text,
|
||||||
|
id: Toasts.genId(),
|
||||||
|
options: {
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
|
export function canDeleteReview(review: Review, userId: string) {
|
||||||
|
if (review.senderdiscordid === userId) return true;
|
||||||
|
|
||||||
|
const myId = BigInt(userId);
|
||||||
|
return myId === Devs.mantikafasi.id ||
|
||||||
|
myId === Devs.Ven.id ||
|
||||||
|
myId === Devs.rushii.id;
|
||||||
|
}
|
43
src/plugins/reviewDB/components/MessageButton.tsx
Normal file
43
src/plugins/reviewDB/components/MessageButton.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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 { classes, LazyComponent } from "@utils/misc";
|
||||||
|
import { findByProps } from "@webpack";
|
||||||
|
|
||||||
|
export default LazyComponent(() => {
|
||||||
|
const { button, dangerous } = findByProps("button", "wrapper", "disabled");
|
||||||
|
|
||||||
|
return function MessageButton(props) {
|
||||||
|
return props.type === "delete"
|
||||||
|
? (
|
||||||
|
<div className={classes(button, dangerous)} aria-label="Delete Review" onClick={props.callback}>
|
||||||
|
<svg aria-hidden="false" width="16" height="16" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
|
||||||
|
<path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className={button} aria-label="Report Review" onClick={() => props.callback()}>
|
||||||
|
<svg aria-hidden="false" width="16" height="16" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M20,6.002H14V3.002C14,2.45 13.553,2.002 13,2.002H4C3.447,2.002 3,2.45 3,3.002V22.002H5V14.002H10.586L8.293,16.295C8.007,16.581 7.922,17.011 8.076,17.385C8.23,17.759 8.596,18.002 9,18.002H20C20.553,18.002 21,17.554 21,17.002V7.002C21,6.45 20.553,6.002 20,6.002Z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
45
src/plugins/reviewDB/components/ReviewBadge.tsx
Normal file
45
src/plugins/reviewDB/components/ReviewBadge.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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 { MaskedLinkStore, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
import { Badge } from "../entities/Badge";
|
||||||
|
|
||||||
|
export default function ReviewBadge(badge: Badge) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
text={badge.badge_name}>
|
||||||
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
<img
|
||||||
|
width="24px"
|
||||||
|
height="24px"
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
src={badge.badge_icon}
|
||||||
|
alt={badge.badge_description}
|
||||||
|
style={{ verticalAlign: "middle", marginLeft: "4px" }}
|
||||||
|
onClick={() =>
|
||||||
|
MaskedLinkStore.openUntrustedLink({
|
||||||
|
href: badge.redirect_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
125
src/plugins/reviewDB/components/ReviewComponent.tsx
Normal file
125
src/plugins/reviewDB/components/ReviewComponent.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* 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 { classes, LazyComponent } from "@utils/misc";
|
||||||
|
import { filters, findBulk } from "@webpack";
|
||||||
|
import { Alerts, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
|
import { Review } from "../entities/Review";
|
||||||
|
import { deleteReview, reportReview } from "../Utils/ReviewDBAPI";
|
||||||
|
import { canDeleteReview, openUserProfileModal, showToast } from "../Utils/Utils";
|
||||||
|
import MessageButton from "./MessageButton";
|
||||||
|
import ReviewBadge from "./ReviewBadge";
|
||||||
|
|
||||||
|
export default LazyComponent(() => {
|
||||||
|
// this is terrible, blame mantika
|
||||||
|
const p = filters.byProps;
|
||||||
|
const [
|
||||||
|
{ cozyMessage, buttons, message, groupStart },
|
||||||
|
{ container, isHeader },
|
||||||
|
{ avatar, clickable, username, messageContent, wrapper, cozy },
|
||||||
|
{ contents },
|
||||||
|
buttonClasses,
|
||||||
|
{ defaultColor }
|
||||||
|
] = findBulk(
|
||||||
|
p("cozyMessage"),
|
||||||
|
p("container", "isHeader"),
|
||||||
|
p("avatar", "zalgo"),
|
||||||
|
p("contents"),
|
||||||
|
p("button", "wrapper", "disabled"),
|
||||||
|
p("defaultColor")
|
||||||
|
);
|
||||||
|
|
||||||
|
return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) {
|
||||||
|
function openModal() {
|
||||||
|
openUserProfileModal(review.senderdiscordid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function delReview() {
|
||||||
|
Alerts.show({
|
||||||
|
title: "Are you sure?",
|
||||||
|
body: "Do you really want to delete this review?",
|
||||||
|
confirmText: "Delete",
|
||||||
|
cancelText: "Nevermind",
|
||||||
|
onConfirm: () => {
|
||||||
|
deleteReview(review.id).then(res => {
|
||||||
|
if (res.successful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
showToast(res.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportRev() {
|
||||||
|
Alerts.show({
|
||||||
|
title: "Are you sure?",
|
||||||
|
body: "Do you really you want to report this review?",
|
||||||
|
confirmText: "Report",
|
||||||
|
cancelText: "Nevermind",
|
||||||
|
// confirmColor: "red", this just adds a class name and breaks the submit button guh
|
||||||
|
onConfirm: () => reportReview(review.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes(cozyMessage, wrapper, message, groupStart, cozy, "user-review")} style={
|
||||||
|
{
|
||||||
|
marginLeft: "0px",
|
||||||
|
paddingLeft: "52px",
|
||||||
|
paddingRight: "16px"
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
|
||||||
|
<div className={contents} style={{ paddingLeft: "0px" }}>
|
||||||
|
<img
|
||||||
|
className={classes(avatar, clickable)}
|
||||||
|
onClick={openModal}
|
||||||
|
src={review.profile_photo || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
|
||||||
|
style={{ left: "0px" }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={classes(clickable, username)}
|
||||||
|
style={{ color: "var(--channels-default)", fontSize: "14px" }}
|
||||||
|
onClick={() => openModal()}
|
||||||
|
>
|
||||||
|
{review.username}
|
||||||
|
</span>
|
||||||
|
{review.badges.map(badge => <ReviewBadge {...badge} />)}
|
||||||
|
<p
|
||||||
|
className={classes(messageContent, defaultColor)}
|
||||||
|
style={{ fontSize: 15, marginTop: 4 }}
|
||||||
|
>
|
||||||
|
{review.comment}
|
||||||
|
</p>
|
||||||
|
<div className={classes(container, isHeader, buttons)} style={{
|
||||||
|
padding: "0px",
|
||||||
|
}}>
|
||||||
|
<div className={buttonClasses.wrapper} >
|
||||||
|
<MessageButton type="report" callback={reportRev} />
|
||||||
|
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
|
||||||
|
<MessageButton type="delete" callback={delReview} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
94
src/plugins/reviewDB/components/ReviewsView.tsx
Normal file
94
src/plugins/reviewDB/components/ReviewsView.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* 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 { classes, useAwaiter } from "@utils/misc";
|
||||||
|
import { findLazy } from "@webpack";
|
||||||
|
import { Forms, React, Text, UserStore } from "@webpack/common";
|
||||||
|
import type { KeyboardEvent } from "react";
|
||||||
|
|
||||||
|
import { addReview, getReviews } from "../Utils/ReviewDBAPI";
|
||||||
|
import ReviewComponent from "./ReviewComponent";
|
||||||
|
|
||||||
|
const Classes = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
|
export default function ReviewsView({ userId }: { userId: string; }) {
|
||||||
|
const [refetchCount, setRefetchCount] = React.useState(0);
|
||||||
|
const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), {
|
||||||
|
fallbackValue: [],
|
||||||
|
deps: [refetchCount],
|
||||||
|
});
|
||||||
|
const username = UserStore.getUser(userId)?.username ?? "";
|
||||||
|
|
||||||
|
const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
|
||||||
|
|
||||||
|
if (isLoading) return null;
|
||||||
|
|
||||||
|
function onKeyPress({ key, target }: KeyboardEvent<HTMLTextAreaElement>) {
|
||||||
|
if (key === "Enter") {
|
||||||
|
addReview({
|
||||||
|
userid: userId,
|
||||||
|
comment: (target as HTMLInputElement).value,
|
||||||
|
star: -1
|
||||||
|
}).then(res => {
|
||||||
|
if (res === 0 || res === 1) {
|
||||||
|
(target as HTMLInputElement).value = ""; // clear the input
|
||||||
|
dirtyRefetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ReviewDB">
|
||||||
|
<Text
|
||||||
|
tag="h2"
|
||||||
|
variant="eyebrow"
|
||||||
|
style={{
|
||||||
|
marginBottom: "12px",
|
||||||
|
color: "var(--header-primary)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
User Reviews
|
||||||
|
</Text>
|
||||||
|
{reviews?.map(review =>
|
||||||
|
<ReviewComponent
|
||||||
|
key={review.id}
|
||||||
|
review={review}
|
||||||
|
refetch={dirtyRefetch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{reviews?.length === 0 && (
|
||||||
|
<Forms.FormText style={{ padding: "12px", paddingTop: "0px", paddingLeft: "4px", fontWeight: "bold", fontStyle: "italic" }}>
|
||||||
|
Looks like nobody reviewed this user yet. You could be the first!
|
||||||
|
</Forms.FormText>
|
||||||
|
)}
|
||||||
|
<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={reviews?.some(r => r.senderdiscordid === UserStore.getCurrentUser().id) ? `Update review for @${username}` : `Review @${username}`}
|
||||||
|
onKeyDown={onKeyPress}
|
||||||
|
style={{
|
||||||
|
marginTop: "6px",
|
||||||
|
resize: "none",
|
||||||
|
marginBottom: "12px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Vencord, a modification for Discord's desktop app
|
* Vencord, a modification for Discord's desktop app
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -16,10 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isTruthy<T>(item: T): item is Exclude<T, 0 | "" | false | null | undefined> {
|
|
||||||
return Boolean(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNonNullish<T>(item: T): item is Exclude<T, null | undefined> {
|
export interface Badge {
|
||||||
return item != null;
|
badge_name: string;
|
||||||
|
badge_description: string;
|
||||||
|
badge_icon: string;
|
||||||
|
redirect_url: string;
|
||||||
|
badge_type: number;
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user