Compare commits

...

80 Commits

Author SHA1 Message Date
V
c696c186e8 WebKeybinds: Fix scope 2023-07-02 01:23:51 +02:00
V
30d5e2108f Bump to 1.3.2 2023-07-02 01:22:53 +02:00
V
1eabd1b701 WebKeybinds: Fix broken zoom on vesktop 2023-07-02 01:22:37 +02:00
V
1cbf2b43e1 Fix Badges 2023-07-02 01:17:02 +02:00
V
4c197d5d51 New Plugin: WebKeyBinds 2023-07-02 01:08:06 +02:00
V
f89027f46a Bump to 1.3.1 2023-06-30 17:08:57 +02:00
V
07a0ebb1d2 PronounDB: Fix crash when having pronouns in profile disabled 2023-06-30 17:08:43 +02:00
V
f09b44b0d5 VcNarrator: Ignore stage channels 2023-06-30 15:55:20 +02:00
V
b607eebcb7 ImageZoom: Add square lens option 2023-06-30 15:50:56 +02:00
V
0936ca2985 ValidUser: Fix mentions with ! (<@!...>) 2023-06-30 15:43:21 +02:00
V
13bde79ec8 PronounDB: Fix profile patch, add pronoun source to tooltip 2023-06-30 15:39:33 +02:00
V
b592defaaf OpenInApp: Add Epic Games; properly respect settings 2023-06-30 15:16:42 +02:00
V
73354973a3 OpenInApp: Support steam; integrate with SpotifyControls & ShowConnections 2023-06-29 16:15:07 +02:00
V
e12c0e546c PronounDB: Fix crash 2023-06-29 14:28:16 +02:00
V
088a8bd1b6 OpenInApp: Support profile activity on web & profile connection 2023-06-29 05:02:13 +02:00
V
51adb26d01 New plugin: OpenInApp - Open spotify urls in spotify app 2023-06-29 04:33:35 +02:00
Nuckyz
cb980a1cad Fix broken SHC patches (#1360) 2023-06-28 03:45:16 +00:00
V
69b10c1f07 Bump to 1.3.0 2023-06-27 23:12:56 +02:00
alexia
8e9ba7c7ee PronounDB: fix caching not respecting user preference (#1344) 2023-06-27 22:46:52 +02:00
Commandtechno
12e3c9234d ViewIcons: Fix animated server pfps & profile edit button (#1350)
fixes #1236
2023-06-27 22:41:52 +02:00
Sammy
1d8dcef394 InvisibleChat: fix indicator & crashes (#1356) 2023-06-27 22:41:33 +02:00
Syncx
4fe2845234 ImageZoom: add nearest neighbour (#1341) 2023-06-27 22:41:19 +02:00
V
5e71ed286e PronounDB: Strip newlines from Discord Pronouns (Discord is stupid) 2023-06-25 19:08:18 +02:00
V
5edbd2391d Fix build 2023-06-25 18:33:44 +02:00
ActuallyTheSun
8472c3823e feat(🗿): ignore blocked users (#1327)
Co-authored-by: V <vendicated@riseup.net>
2023-06-25 18:31:39 +02:00
V
2103e52115 WebContextMenus: Support all text areas 2023-06-25 18:22:36 +02:00
Nuckyz
afbfb641e8 Fix IgnoreActivities broken patch (#1337) 2023-06-25 09:57:18 +00:00
V
d7ac418e05 Fix some plugins displaying legacy discriminators (username#0000) 2023-06-23 18:09:43 +02:00
V
214c101740 Bump to v1.2.9 2023-06-22 22:45:28 +02:00
V
5a0e501829 Fix ValidUser, BetterFolders & MutualGroupDms
Co-authored-by: Juby210 <31005896+Juby210@users.noreply.github.com>
Co-authored-by: Amia <9750071+aamiaa@users.noreply.github.com>
2023-06-22 22:35:59 +02:00
V
92113da7c0 Fix Vencord Web 2023-06-21 21:50:28 +02:00
V
96f30a5359 Update README.md 2023-06-21 21:07:48 +02:00
V
ceb1f15188 utils: export missing members 2023-06-21 21:04:04 +02:00
V
626eb3613e [skip ci] WelcomeStickerPicker: Clean up settings types 2023-06-21 02:05:01 +02:00
V
3020fcc9bb [skip ci] Improve typings for settings.withPrivateSettings 2023-06-21 02:00:59 +02:00
heckeroncrack
bc0de3926c Improve uwuify (#1295)
Co-authored-by: V <vendicated@riseup.net>
2023-06-20 22:11:59 +02:00
Nuckyz
9820b79dfe SHC: show active now voice members; PermViewer: improve styling (#1314) 2023-06-20 22:11:50 +02:00
V
ab811470fc Update README.md 2023-06-18 21:27:52 +02:00
V
e4162e7bd5 PronounDB: Source pronouns from both PronounDB & Discord (#1301)
* PronounDB: Source pronouns from both PronounDB & Discord

* jdsjdsajjasiofigvjodsjigfdjiogegjnegjnersjn
2023-06-17 03:50:59 +02:00
V
7e8397a4da Bump to 1.2.8 2023-06-16 19:47:01 +02:00
V
555cf64080 BiggerStreamPreview: Hide element if no stream is found 2023-06-16 19:46:39 +02:00
V
2039e10fd5 Codeberg Mirror: Hopefully fix race condition 2023-06-16 19:45:01 +02:00
Phil
e8d90d2b45 feat(plugin): BiggerStreamPreview (#1222)
Co-authored-by: V <vendicated@riseup.net>
2023-06-16 19:35:50 +02:00
kb
55af40ee74 MessageLogger: Add user ignore list (#1275)
Co-authored-by: V <vendicated@riseup.net>
2023-06-16 19:35:35 +02:00
V
a1fabcdf0a UnsuppressEmbeds: Support dms 2023-06-16 19:28:30 +02:00
V
eaeb60308e [skip ci] Add version to /vencord-debug 2023-06-16 19:07:22 +02:00
Amia
662c0227eb New Plugin: MutualGroupDMs (#1239)
Co-authored-by: V <vendicated@riseup.net>
2023-06-15 03:39:15 +02:00
Dominik
543fdf4943 New Plugin: UnsuppressEmbeds (#1262)
Co-authored-by: V <vendicated@riseup.net>
2023-06-15 03:18:34 +02:00
Nuckyz
1225383723 Fix broken SHC patch (#1290) 2023-06-15 01:05:15 +00:00
V
07a9adbce2 🧹🧹 2023-06-13 03:45:05 +02:00
V
42d8211871 Fix disabling plugins whose stop() errors 2023-06-13 02:29:29 +02:00
V
ab3e993274 ViewRaw: Remove less properties from author 2023-06-13 02:23:06 +02:00
rad
386dfe363a ViewRaw: Add setting for swapping left/right click (#1263)
Co-authored-by: V <vendicated@riseup.net>
2023-06-13 01:05:04 +02:00
AAGaming
a4191c9f6c Settings: add custom sections support (#1270)
Co-authored-by: V <vendicated@riseup.net>
2023-06-11 23:11:56 +02:00
V
f1349a2787 Remove Cloud Sync notification 2023-06-11 22:57:07 +02:00
V
3680c26f72 Bump to 1.2.7 2023-06-09 22:18:32 +02:00
Axu
683c92f904 fix: do not highlight new member leaf in red in message logger (#1071)
Co-authored-by: V <vendicated@riseup.net>
2023-06-09 22:17:44 +02:00
Rendundakat
3410ed024f QuickReply: respect users in userList of NoReplyMention (#1191)
Co-authored-by: V <vendicated@riseup.net>
2023-06-09 22:17:32 +02:00
Luna
dbad10984a [skip ci] Don't run ci on forks; Fix interaction nicks in SMYN (#4)
The following actions are disabled unless run on the main repo: codeberg, publish, build/upload, reportBrokenPlugins

Reviewed-on: https://codeberg.org/Ven/cord/pulls/4
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-06-09 19:48:18 +00:00
whqwert
55543d8640 Fix(RevealAllSpoilers): New spoiler class name (#1229) 2023-06-04 17:22:21 -03:00
Manti
263fbc377e Fix ReviewDB auth (#1227) 2023-06-03 20:07:04 +00:00
AutumnVN
c9c0ab5aca Fix cloud auth [object%20Object] (#1226) 2023-06-03 16:57:31 -03:00
V
7b2bf08b8f [skip ci] ci: fix inconsistent formatting 2023-06-01 22:49:15 +02:00
TheKodeToad
43011825af Add NoProfileThemes plugin (#1193)
Co-authored-by: V <vendicated@riseup.net>
2023-05-31 19:14:18 +02:00
fawn
4abcea61f8 feat(MessageClickAction): delete message logger history (#1204) 2023-05-31 19:14:17 +02:00
V
cba810cab5 Improve settings selectable text style 2023-05-31 19:14:17 +02:00
Amia
5938c7d67c Plugin: NoPendingCount (#1216)
Co-authored-by: V <vendicated@riseup.net>
2023-05-31 19:14:17 +02:00
V
99d8b8b75f VcNarrator: Fix always saying 'someone' 2023-05-31 19:14:14 +02:00
Luna
503d49d295 Add codeberg to gitlens and add build tasks (#3)
It needed to be added inorder for gitlens to recognise the remote as a gitea instance
without, it wouldnt be able to open branches/specific commits correctly

and build tasks are just for ease of development

(teeny weeny pr bc im not gonna do anything else with this branch)

Co-authored-by: Luna R <imlvnaa@gmail.com>
Reviewed-on: https://codeberg.org/Ven/cord/pulls/3
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-05-31 17:12:37 +00:00
V
137b79d95b README: Add codeberg mirror 2023-05-30 16:27:59 +02:00
V
3c02d6e1b4 Bump to 1.2.6 2023-05-30 15:24:21 +02:00
V
a2a33ca62d Fix occasional freezing on firefox (cache related) 2023-05-30 15:23:33 +02:00
V
d8cd557fb2 Clean up build scripts 2023-05-30 15:23:32 +02:00
V
7568bbaed0 VcNarrator: Improve username cleaning to support non ascii chars 2023-05-30 15:23:32 +02:00
V
9023d45d9e Improve /vencord-debug 2023-05-30 15:23:32 +02:00
V
bee70390a9 Add codeberg mirror sync workflow 2023-05-29 23:50:45 +02:00
Manti
3e3d05fc26 ReviewDB: Add Review Modal & Pagination (#1174)
Co-authored-by: V <vendicated@riseup.net>
2023-05-28 22:03:06 +02:00
V
6300198a54 MessageLinkEmbeds: Fix niche unclaimed account bug
This plugin fires MESSAGE_UPDATE events for messages containing message
links (to rerender them). If the updated message is an interaction, it
contains message.interaction.user. If the one who ran the command is
you, message.interaction.user will be you and the email in this data is
always set to null. Discord seems to update the local user data with
this user. So essentially, in the above described edge case it would
update the current user to have no email (only locally, in memory. There
is 0 risk for your account, it was just a temporary visual bug) which
would cause the unclaimed account banner to appear.

This commit fixes this by simply omitting the interaction field from the
MESSAGE_UPDATE event
2023-05-28 22:02:47 +02:00
Nuckyz
458c7ed4c5 Make Fake Nitro transformations support the new markdown (#911) 2023-05-23 00:25:48 -03:00
V
d888a0a291 [skip ci] Fix plugin json generation 2023-05-23 04:42:06 +02:00
129 changed files with 2638 additions and 958 deletions

View File

@ -42,37 +42,39 @@ jobs:
- name: Clean up obsolete files
run: |
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
- name: Get some values needed for the release
id: release_values
run: |
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Upload DevBuild as release
if: github.repository == 'Vendicated/Vencord'
run: |
gh release upload devbuild --clobber dist/*
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
gh release upload devbuild --clobber dist/*
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ env.release_tag }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ env.release_tag }}
- name: Upload DevBuild to builds repo
if: github.repository == 'Vendicated/Vencord'
run: |
git config --global user.name "$USERNAME"
git config --global user.email actions@github.com
git config --global user.name "$USERNAME"
git config --global user.email actions@github.com
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload
cd upload
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload
cd upload
GLOBIGNORE=.git:.gitignore:README.md:LICENSE
rm -rf *
cp -r ../dist/* .
GLOBIGNORE=.git:.gitignore:README.md:LICENSE
rm -rf *
cp -r ../dist/* .
git add -A
git commit -m "Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
git add -A
git commit -m "Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
env:
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
GH_REPO: Vencord/builds
USERNAME: GitHub-Actions
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
GH_REPO: Vencord/builds
USERNAME: GitHub-Actions

22
.github/workflows/codeberg-mirror.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Sync to Codeberg
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
on:
push:
workflow_dispatch:
schedule:
- cron: "0 */6 * * *"
jobs:
codeberg:
if: github.repository == 'Vendicated/Vencord'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
with:
target_repo_url: "git@codeberg.org:Ven/cord.git"
ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }}

View File

@ -6,6 +6,7 @@ on:
jobs:
Publish:
if: github.repository == 'Vendicated/Vencord'
runs-on: ubuntu-latest
steps:
@ -13,11 +14,11 @@ jobs:
- name: check that tag matches package.json version
run: |
pkg_version="v$(jq -r .version < package.json)"
if [[ "${{ github.ref_name }}" != "$pkg_version" ]]; then
echo "Tag ${{ github.ref_name }} does not match package.json version $pkg_version" >&2
exit 1
fi
pkg_version="v$(jq -r .version < package.json)"
if [[ "${{ github.ref_name }}" != "$pkg_version" ]]; then
echo "Tag ${{ github.ref_name }} does not match package.json version $pkg_version" >&2
exit 1
fi
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
@ -35,26 +36,26 @@ jobs:
- name: Publish extension
run: |
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
EXIT_CODE=0
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
EXIT_CODE=0
# Chrome
cd dist/chromium-unpacked
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
# Chrome
cd dist/chromium-unpacked
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
# Firefox
cd ../firefox-unpacked
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
web-ext-submit || EXIT_CODE=$?
# Firefox
cd ../firefox-unpacked
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
web-ext-submit || EXIT_CODE=$?
exit $EXIT_CODE
exit $EXIT_CODE
env:
# Chrome
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
# Chrome
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
# Firefox
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}
# Firefox
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}

View File

@ -2,11 +2,12 @@ name: Test Patches
on:
workflow_dispatch:
schedule:
# Every day at midnight
- cron: 0 0 * * *
# Every day at midnight
- cron: 0 0 * * *
jobs:
TestPlugins:
if: github.repository == 'Vendicated/Vencord'
runs-on: ubuntu-latest
steps:
@ -22,10 +23,10 @@ jobs:
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
pnpm add puppeteer
pnpm install --frozen-lockfile
pnpm add puppeteer
sudo apt-get install -y chromium-browser
sudo apt-get install -y chromium-browser
- name: Build web
run: pnpm buildWeb --standalone
@ -33,25 +34,25 @@ jobs:
- name: Create Report
timeout-minutes: 10
run: |
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- name: Create Report (Canary)
timeout-minutes: 10
if: success() || failure() # even run if previous one failed
run: |
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
export USE_CANARY=true
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
export USE_CANARY=true
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

View File

@ -12,5 +12,12 @@
"javascript.format.semicolons": "insert",
"typescript.format.semicolons": "insert",
"typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double"
"javascript.preferences.quoteStyle": "double",
"gitlens.remotes": [
{
"domain": "codeberg.org",
"type": "Gitea"
}
]
}

25
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"type": "shell",
"command": "pnpm build",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Watch",
"type": "shell",
"command": "pnpm watch",
"problemMatcher": [],
"group": {
"kind": "build"
}
}
]
}

View File

@ -1,7 +1,8 @@
# Vencord
The cutest Discord client mod
[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Ven/cord&color=2185D0&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAKbUlEQVR4nNVae3AV5RX/nW/3Pva+b24e5HHzIICQKGoiYiW8NFBFgohaa6ctglpbFSujSGurzUinohWsOij/gGX6R2fqOK0d1FYTEZXaTrWCBbEikJCEyCvkeXNvkrunf+zdkJDkPnex/c3cmd29+53v/M6e73znnF2Cydj4Tntldzi6qrN/qKqzf2jy6b7BnL4B1dI7oMp9AyoRAIdVsNMqhlxWMZjtspzyK/Jhr036OMsm//bh2vzPzNSPzBD6xFutd7R0Dq758ky4orkjYuc05RCAkixbeEq2/UCJ1/LczxcX/c5IPfU5DMHmxpbCpu7o1k/b+xc1n43YjJI7EqV+W2RmvuPt0oDjB2vn5bQbITNjAzzdeKK8qTO0bU9T77zucNQUjzofHrvENWWu3aUBZfW6+ZOOZiIrbYXrmUXo9daX3v6i667O/iGRiRLpwqtIvKDc+0efJ3hb/UIaSkdGWgZ4sqGt9r2m3lc/P9HvSWe80ZiRp3TPL/UsX1+bvyvVsSkb4NE3WjbuPNj5SM8Fcvdk4bAKrqvwv7DxhuCPUxmXNIn6XSy3nWr6R8OhrqrU1btwqJ3m/bgwu/SqZJdEUgbYsuuka09b9/4Pm3tLMlPvwuAbpe6m+RcplfdcURBKdG9CA2zZddLV2Nx1+JO2vlxj1LswqCpynlxc6SxLZIS40bueWfy9vXvv/xt5APhXa1/u7v+EPqvfxXK8++IaoO2Vpn9+cLS33FjVLhw+bOotOX7q6N/i3TOhAX7y+rHN/+sBLxm8fah71k93tjw/0f/jGuDJxtZrdh7setA8tS4sdn7eef+v3mmfP95/Ywxw6x9Yev9I35/6Iubv83WVfl5a6Uu3VkoavZEo7TnS/Vo98xi+Yy6UKC3bDp7sd5ut1OWFDjyzNMib6oq5Oug0ezp8dqLfG3r92Nbzr48ywNONJ8obDnV/z2xlAk4ZW1aUqhaJIAvCb5YVqwFn3GBtCBoO9dz5TOPxUbnMKAM0dYa2d5lc2AgCNi8r5klui3aBgWynjE11QZbI3FV3NjQkjnYNbB+lj36wubGlcE9T71xTNQDw0Px8nlvmHl73GmfCrKCL19Tkmh4P9jT1LHz2vVP5+vmwAZq71a1m1/PXTPXwD68eS5KIEVUZd1yZwwumeEw1Qld/lJrPhF7Sz4cNsO+rUK2ZExd6rfj10iCPZ2GJCCoAZuCJxQUc9FvNVAX72kPX6ccC0Hp4zR0Ru1kT2mTCSzeXqn5l/EAniMAqoDLDYZWwqa5EVSzmhaKmsxHbLxvbbgdiBmjpHFxj2mwANlxXxBdPUib8nwgQgqAyEFUZxT4L1i/MN3UpHDsTWQvEDHDoTLjCrIluuyzAt8zMSkhGFhp5hrYUFk3z8IqZftOMcKRj4GIAEM80tFccM8n9Z+Qq+MXigqRIWCQCMzQvYIbKwH1X53FFnjkr88iZsLKpoXWa6BiIrjbDzF67hK23lKp2Obm1LAstPEZVjTwDkAio/2ZQ9dolw/VjAB0DfKfoCg9WGy2cADy1NMhBX2rR3CIRGICq8rAhAg4Jj9UWsDBhg+4MR6vF2VC0zGjB99fk8eJp3pQdyyrRMHF9KURVxswCB6+alWO4o3b2RyeLU32D2UYKnVPm5gfm5qWlrF0Wo4hzbCmoDNw0089XlboNNcLpvsFc0RtRDXuNle+x4Lkbi9PO6WWJIBFGEY+qjGjswtq5eVzosRilLnoiUavoH1INiTCyIDy/vETNcmRW1dl0L4gRVxmx3YFhlwnrry1QrZIxASE0yJIIDaiGSHt8UQFXF2Ve1zusYgzxkXGhyGvFvePUE+mgfyAqhGqAqKWVPv5udbYhSjmtkpYWq6OJqzFjqCpjTpmbl1Rk3klSGRBWmTISNC3Hjo1LgoYFJ0GA1aIVR+cTVxlQoS2Pb18a4PLszMKXzSJYuCySmq4Al03CiytKVYfBhYvLKk1IXE+XLRLhwZp81WlNf26HTFHhd0jhdAYTgKduCPLkgPHfQjitYkLiAIEZBDBlu2R6aF7euCV2Mgg45bDw2qWOdAavnp3D109PPdlJBvpTnYg4kVY3MDMuylVw62WJi63x4LHLZ0TAIR9OdWBVodPUclUQwWmT4hLXfgCIUDfDi6oiR8rzBJzyl8LnkD9KZVCOU8aLN5eoshnJ+Qh4bFJC4gztmEjgrtk5anaKnWWfXfpIuBTLjmSpSILw/E0laq7LuGxsIngVCYmIa96hLRG3TaZ1C/KTfjAEQLFIO8TPFk7aH/RZI8kMWrdgEs8udqXLKSUoMkEW4ETEQTRsoHyPlVZfmVw+Uuy3hR9bVHBQAMD0XPu/Ew24dqqH777K/La1DiKCxyYlRRzQymgG4+oyDxZOTdxZnp5r3wvEWmJ5btuL8W4uzbJh87LitLebdOFVpKSJx4IlwIzbL81CcYLO8iSX/IImGQCYae6Wg/2tXQNjNnW7LPDKyilqZd7ETU2zEBlifNTSS4i9PNFIx44x4jh2nZlBsUr0dN8QP/6XVhEaHJvnlfhtkXd/NF0BUextKRFXFznfGk+JDdcX8tdBHtDa6YpFsB4I9ac88omf8wbEgqa2XAIOme6bM35foqrQ+QZIKwGG80ifVbrXZZNGDfhOVYBvviS9JMMoaP3AEcQpPnHdOxiMGXkKbrx4dGfZY5c4T8H9+vmwAeqXFLXOKXW9r59fWuDA44sKv1byAOBzyCkTH+kdS2f4MLPgXJI0p9T17vrFxcf181GVxEUB+0qfIqt+RcKWFSWGNR4ygd4RTpW4HiCJgFWzstmnSPA7ZLU827pypPwxDB/687GXl1X6Vs6bbGz/LRN80hZCT+yLFZ0cgHED4egACeiXm89GsP9EePuzy4rvGil7jAGYmQDsBjDHUBYZ4GhHBMfORigd4rpnyIS9u6d4rqgnGrUtjCmmSYuOqwB0GcwjbWh9xviurpNnxnDA1IspMPe6bOL755MHJvhKjIgOA7jbJD4pw22Thj+kSIW47h2KRaydVezeP57sCdspRPQqgGeNJJIuBAE+ReJUiOv32mXaXjPZs21C2QnmXgdghyEsMoRfkVMiDgCywF/by9z3xJMb1wCxeHAPgDczZpAh/Iq+HSYmDjCsstgThmf5t4ii8eQm7CgS0SCA5QBezoRApnBaBSyCEhIHCLJEb4ZUd+2SqZSwzE+qpUpEQ9CC4qb01M8cRIQsh8zxiKsMtsn08nvlnrpkyAPj5AGJwMw3AtgGwJ/q2ExxvHsQB74KxfKBMblAyGmTHq4pc4/5GjQeUm6qE9FrAK4E8H6ie41GlkN/jTk6F5Ak2ueUpNmpkgfSMAAAENERAAsB3AHgZDoy0oFdFnBYpXPEBfU4beLRD6Z4qmumug+kIzPjaoeZfQDWAHgAQFam8hLh4MkwWjsHemyS2OF08IYrCjynzZ4zKTCzi5nXMvOnzBw16bevIxR95JOj7DNKb1PqXWa+HMDtAGoBXII0lxq0N2OfAmgA8Hsi2muMhudgesHPzNkA5gKoADADwFRoS8UHQO+x9wLoBNAB4AsAnwM4AOADIjLVxf8L9kdXUOE0IskAAAAASUVORK5CYII=)](https://codeberg.org/Ven/cord)
The cutest Discord client mod
![](https://user-images.githubusercontent.com/45497981/235015332-0453d3eb-1da6-4601-963e-ef5e454123a1.png)
*A screenshot of Vencord featuring the [ClearVision-v6 theme](https://github.com/ClearVision/ClearVision-v6) (Vencord does not come with it pre-installed, it is only an example)*
@ -38,10 +39,9 @@ Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Ve
## Vencord Desktop
> **Warning**
> This is an alternative app. It currently doesn't support screensharing or keybinds. If you just want to install to the normal Discord Desktop app, scroll up
> This is an alternative app. It currently doesn't support keybinds and possibly some more features. If you just want to install to the normal Discord Desktop app, scroll up
As an alternative to the Discord Desktop app, Vencord also has its own standalone Desktop app that is snappier and lighter than Discord's official Desktop app. It is currently in beta and we have yet to implement some features like screensharing, but you can try the beta nonetheless
As an alternative to the Discord Desktop app, Vencord also has its own standalone Desktop app that is snappier and lighter than Discord's official Desktop app
[![Download Vencord Desktop](https://img.shields.io/github/v/release/Vencord/Desktop?label=Download%20Vencord%20Desktop&style=for-the-badge)](https://github.com/Vencord/Desktop#vencord-desktop)
@ -49,7 +49,7 @@ As an alternative to the Discord Desktop app, Vencord also has its own standalon
## Join our Support/Community Server
[![Vencord Discord Server](https://invidget.switchblade.xyz/D9uwnFnqmd?theme=dark)](https://discord.gg/D9uwnFnqmd)
https://discord.gg/D9uwnFnqmd
## Disclaimer

View File

@ -41,12 +41,5 @@
"path": "modifyResponseHeaders.json"
}
]
},
"browser_specific_settings": {
"gecko": {
"id": "vencord-firefox@vendicated.dev",
"strict_min_version": "109.0"
}
}
}

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.2.5",
"version": "1.3.2",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -24,7 +24,7 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix",
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit",
"uninject": "node scripts/runInstaller.mjs",

View File

@ -19,11 +19,13 @@
import esbuild from "esbuild";
import { commonOpts, globPlugins, isStandalone, watch } from "./common.mjs";
import { commonOpts, globPlugins, isStandalone, VERSION, watch } from "./common.mjs";
const defines = {
IS_STANDALONE: isStandalone,
IS_DEV: JSON.stringify(watch)
IS_DEV: JSON.stringify(watch),
VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP: Date.now(),
};
if (defines.IS_STANDALONE === "false")
// If this is a local build (not standalone), optimise

View File

@ -24,9 +24,7 @@ import { readFileSync } from "fs";
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
import { join } from "path";
// wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" };
import { commonOpts, globPlugins, watch } from "./common.mjs";
import { commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
@ -47,7 +45,9 @@ const commonOptions = {
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch),
IS_DISCORD_DESKTOP: "false",
IS_VENCORD_DESKTOP: "false"
IS_VENCORD_DESKTOP: "false",
VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP: Date.now(),
}
};
@ -67,7 +67,7 @@ await Promise.all(
},
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${VERSION}.${new Date().getTime()}`)
},
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
@ -88,7 +88,7 @@ async function buildPluginZip(target, files, shouldZip) {
let content = await readFile(join("browser", f));
if (f.startsWith("manifest")) {
const json = JSON.parse(content.toString("utf-8"));
json.version = PackageJSON.version;
json.version = VERSION;
content = new TextEncoder().encode(JSON.stringify(json));
}

View File

@ -16,12 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js";
import { exec, execSync } from "child_process";
import { existsSync, readFileSync } from "fs";
import { readdir, readFile } from "fs/promises";
import { join, relative } from "path";
import { promisify } from "util";
// wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" };
export const VERSION = PackageJSON.version;
export const BUILD_TIMESTAMP = Date.now();
export const watch = process.argv.includes("--watch");
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
@ -62,7 +70,7 @@ export const globPlugins = kind => ({
});
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
const pluginDirs = ["plugins", "userplugins"];
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"];
let code = "";
let plugins = "\n";
let i = 0;
@ -70,8 +78,9 @@ export const globPlugins = kind => ({
if (!existsSync(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`);
for (const file of files) {
if (file.startsWith(".")) continue;
if (file.startsWith("_") || file.startsWith(".")) continue;
if (file === "index.ts") continue;
const fileBits = file.split(".");
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
const mod = fileBits.at(-2);

View File

@ -1,62 +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/>.
*/
// A script to automatically generate a list of all plugins.
// Just copy paste the entire file into a running Vencord install and it will prompt you
// to save the file
// eslint-disable-next-line spaced-comment
/// <reference types="../src/modules"/>
(() => {
/**
* @type {typeof import("~plugins").default}
*/
const Plugins = Vencord.Plugins.plugins;
const header = `
<!-- This file is auto generated, do not edit -->
# Vencord Plugins
`;
let tableOfContents = "\n\n";
let list = "\n\n";
for (const p of Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name))) {
tableOfContents += `- [${p.name}](#${p.name.replaceAll(" ", "-")})\n`;
list += `## ${p.name}
${p.description}
**Authors**: ${p.authors.map(a => a.name).join(", ")}
`;
if (p.commands?.length) {
list += "\n\n#### Commands\n";
for (const cmd of p.commands) {
list += `${cmd.name} - ${cmd.description}\n\n`;
}
}
list += "\n\n";
}
copy(header + tableOfContents + list);
})();

View File

@ -19,7 +19,7 @@
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";
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
interface Dev {
name: string;
@ -66,9 +66,9 @@ function parseDevs() {
const value = devsDeclaration.initializer.arguments[0];
if (!isObjectLiteralExpression(value)) return;
if (!isSatisfiesExpression(value) || !isObjectLiteralExpression(value.expression)) throw new Error("Failed to parse devs: not an object literal");
for (const prop of value.properties) {
for (const prop of value.expression.properties) {
const name = (prop.name as Identifier).text;
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
@ -130,7 +130,9 @@ async function parseFile(fileName: string) {
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)!];
const d = devs[getName(e)!];
if (!d) throw fail(`couldn't look up author ${getName(e)}`);
return d;
});
break;
case "tags":
@ -149,7 +151,6 @@ async function parseFile(fileName: string) {
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;
}
}
@ -169,8 +170,8 @@ async function parseFile(fileName: string) {
throw fail("no default export called 'definePlugin' found");
}
async function getEntryPoint(dirent: Dirent) {
const base = join("./src/plugins", dirent.name);
async function getEntryPoint(dir: string, dirent: Dirent) {
const base = join(dir, dirent.name);
if (!dirent.isDirectory()) return base;
for (const name of ["index.ts", "index.tsx"]) {
@ -184,13 +185,23 @@ async function getEntryPoint(dirent: Dirent) {
throw new Error(`${dirent.name}: Couldn't find entry point`);
}
function isPluginFile({ name }: { name: string; }) {
if (name === "index.ts") return false;
return !name.startsWith("_") && !name.startsWith(".");
}
(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 plugins = ["src/plugins", "src/plugins/_core"].flatMap(dir =>
readdirSync(dir, { withFileTypes: true })
.filter(isPluginFile)
.map(async dirent =>
parseFile(await getEntryPoint(dir, dirent))
)
);
const data = JSON.stringify(await Promise.all(promises));
const data = JSON.stringify(await Promise.all(plugins));
if (process.argv.length > 2) {
writeFileSync(process.argv[2], data);

View File

@ -58,4 +58,10 @@ export default {
getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>,
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
},
pluginHelpers: {
OpenInApp: {
resolveRedirect: (url: string) => invoke<string>(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, url),
},
}
};

View File

@ -22,7 +22,7 @@ import { ComponentType, HTMLProps } from "react";
import Plugins from "~plugins";
export enum BadgePosition {
export const enum BadgePosition {
START,
END
}
@ -79,7 +79,7 @@ export function _getBadges(args: BadgeUserArgs) {
: badges.push({ ...badge, ...args });
}
}
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/apiBadges").default).getDonorBadges(args.user.id);
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.user.id);
if (donorBadges) badges.unshift(...donorBadges);
return badges;

View File

@ -24,7 +24,7 @@ export interface CommandContext {
guild?: Guild;
}
export enum ApplicationCommandOptionType {
export const enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
@ -38,7 +38,7 @@ export enum ApplicationCommandOptionType {
ATTACHMENT = 11,
}
export enum ApplicationCommandInputType {
export const enum ApplicationCommandInputType {
BUILT_IN = 0,
BUILT_IN_TEXT = 1,
BUILT_IN_INTEGRATION = 2,
@ -64,7 +64,7 @@ export interface ChoicesOption {
displayName?: string;
}
export enum ApplicationCommandType {
export const enum ApplicationCommandType {
CHAT_INPUT = 1,
USER = 2,
MESSAGE = 3,

View File

@ -20,7 +20,7 @@ import { Logger } from "@utils/Logger";
const logger = new Logger("ServerListAPI");
export enum ServerListRenderPosition {
export const enum ServerListRenderPosition {
Above,
In,
}

View File

@ -254,8 +254,12 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
}
}
export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) {
const definedSettings: DefinedSettings<D> = {
export function definePluginSettings<
Def extends SettingsDefinition,
Checks extends SettingsChecks<Def>,
PrivateSettings extends object = {}
>(def: Def, checks?: Checks) {
const definedSettings: DefinedSettings<Def, Checks, PrivateSettings> = {
get store() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return Settings.plugins[definedSettings.pluginName] as any;
@ -264,9 +268,14 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
).plugins[definedSettings.pluginName] as any,
def,
checks: checks ?? {},
checks: checks ?? {} as any,
pluginName: "",
withPrivateSettings<T extends object>() {
return this as DefinedSettings<Def, Checks, T>;
}
};
return definedSettings;
}

View File

@ -0,0 +1,12 @@
.vc-expandableheader-center-flex {
display: flex;
justify-items: center;
align-items: center;
}
.vc-expandableheader-btn {
all: unset;
cursor: pointer;
width: 24px;
height: 24px;
}

View File

@ -0,0 +1,108 @@
/*
* 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 { classNameFactory } from "@api/Styles";
import { Text, Tooltip, useState } from "@webpack/common";
export const cl = classNameFactory("vc-expandableheader-");
import "./ExpandableHeader.css";
export interface ExpandableHeaderProps {
onMoreClick?: () => void;
moreTooltipText?: string;
onDropDownClick?: (state: boolean) => void;
defaultState?: boolean;
headerText: string;
children: React.ReactNode;
buttons?: React.ReactNode[];
}
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
const [showContent, setShowContent] = useState(defaultState);
return (
<>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "8px"
}}>
<Text
tag="h2"
variant="eyebrow"
style={{
color: "var(--header-primary)",
display: "inline"
}}
>
{headerText}
</Text>
<div className={cl("center-flex")}>
{
buttons ?? null
}
{
onMoreClick && // only show more button if callback is provided
<Tooltip text={moreTooltipText}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("btn")}
onClick={onMoreClick}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
</svg>
</button>
)}
</Tooltip>
}
<Tooltip text={showContent ? "Hide " + headerText : "Show " + headerText}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("btn")}
onClick={() => {
setShowContent(v => !v);
onDropDownClick?.(showContent);
}}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
transform={showContent ? "scale(1 -1)" : "scale(1 1)"}
>
<path fill="var(--text-normal)" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
</svg>
</button>
)}
</Tooltip>
</div>
</div>
{showContent && children}
</>
);
}

View File

@ -146,3 +146,47 @@ export function OwnerCrownIcon(props: IconProps) {
</Icon>
);
}
/**
* Discord's screenshare icon, as seen in the connection panel
*/
export function ScreenshareIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-screenshare-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2 4.5C2 3.397 2.897 2.5 4 2.5H20C21.103 2.5 22 3.397 22 4.5V15.5C22 16.604 21.103 17.5 20 17.5H13V19.5H17V21.5H7V19.5H11V17.5H4C2.897 17.5 2 16.604 2 15.5V4.5ZM13.2 14.3375V11.6C9.864 11.6 7.668 12.6625 6 15C6.672 11.6625 8.532 8.3375 13.2 7.6625V5L18 9.6625L13.2 14.3375Z"
/>
</Icon>
);
}
export function ImageVisible(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-image-visible")}
viewBox="0 0 24 24"
>
<path fill="currentColor" d="M5 21q-.825 0-1.413-.587Q3 19.825 3 19V5q0-.825.587-1.413Q4.175 3 5 3h14q.825 0 1.413.587Q21 4.175 21 5v14q0 .825-.587 1.413Q19.825 21 19 21Zm0-2h14V5H5v14Zm1-2h12l-3.75-5-3 4L9 13Zm-1 2V5v14Z" />
</Icon>
);
}
export function ImageInvisible(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-image-invisible")}
viewBox="0 0 24 24"
>
<path fill="currentColor" d="m21 18.15-2-2V5H7.85l-2-2H19q.825 0 1.413.587Q21 4.175 21 5Zm-1.2 4.45L18.2 21H5q-.825 0-1.413-.587Q3 19.825 3 19V5.8L1.4 4.2l1.4-1.4 18.4 18.4ZM6 17l3-4 2.25 3 .825-1.1L5 7.825V19h11.175l-2-2Zm7.425-6.425ZM10.6 13.4Z" />
</Icon>
);
}

View File

@ -138,11 +138,13 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
}
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
const action = wasEnabled ? "stop" : "start";
if (!result) {
logger.error(`Failed to ${action} plugin ${plugin.name}`);
showErrorToast(`Failed to ${action} plugin: ${plugin.name}`);
settings.enabled = false;
const msg = `Error while ${wasEnabled ? "stopping" : "starting"} plugin ${plugin.name}`;
logger.error(msg);
showErrorToast(msg);
return;
}
@ -171,7 +173,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
);
}
enum SearchStatus {
const enum SearchStatus {
ALL,
ENABLED,
DISABLED

View File

@ -57,7 +57,7 @@
}
.vc-text-selectable,
.vc-text-selectable :not(a, button, a *, button *, input, input *) {
.vc-text-selectable :where([class*="text" i], [class*="title" i]) {
/* make text selectable, silly discord makes the entirety of settings not selectable */
user-select: text;

2
src/globals.d.ts vendored
View File

@ -37,6 +37,8 @@ declare global {
export var IS_STANDALONE: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VENCORD_DESKTOP: boolean;
export var VERSION: string;
export var BUILD_TIMESTAMP: number;
export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord");

View File

@ -17,6 +17,7 @@
*/
import "./updater";
import "./ipcPlugins";
import { debounce } from "@utils/debounce";
import { IpcEvents } from "@utils/IpcEvents";

46
src/main/ipcPlugins.ts Normal file
View File

@ -0,0 +1,46 @@
/*
* 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 { IpcEvents } from "@utils/IpcEvents";
import { ipcMain } from "electron";
import { request } from "https";
// #region OpenInApp
// These links don't support CORS, so this has to be native
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
function getRedirect(url: string) {
return new Promise<string>((resolve, reject) => {
const req = request(new URL(url), { method: "HEAD" }, res => {
resolve(
res.headers.location
? getRedirect(res.headers.location)
: url
);
});
req.on("error", reject);
req.end();
});
}
ipcMain.handle(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, async (_, url: string) => {
if (!validRedirectUrls.test(url)) return url;
return getRedirect(url);
});
// #endregion

View File

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

View File

@ -80,8 +80,8 @@ export default definePlugin({
find: "Messages.PROFILE_USER_BADGES,role:",
replacement: [
{
match: /(?<=(\i)\.isTryItOutFlow,)(.{0,300})null==\i\?void 0:(\i)\.getBadges\(\)/,
replace: (_, props, restCode, badgesMod) => `vencordProps=${props},${restCode}Vencord.Api.Badges._getBadges(vencordProps).concat(${badgesMod}?.getBadges()??[])`,
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
replace: "$&$1.unshift(...Vencord.Api.Badges._getBadges(arguments[0]));",
},
{
// alt: "", aria-hidden: false, src: originalSrc

View File

@ -82,6 +82,8 @@ export default definePlugin({
}
}],
customSections: [] as ((ID: Record<string, unknown>) => any)[],
makeSettingsCategories({ ID }: { ID: Record<string, unknown>; }) {
return [
{
@ -123,11 +125,13 @@ export default definePlugin({
label: "Patch Helper",
element: require("@components/VencordSettings/PatchHelperTab").default,
},
// TODO: make this use customSections
IS_VENCORD_DESKTOP && {
section: "VencordDesktop",
label: "Desktop Settings",
element: VencordDesktop.Components.Settings,
},
...this.customSections.map(func => func(ID)),
{
section: ID.DIVIDER
}

View File

@ -20,7 +20,7 @@ import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
enum Methods {
const enum Methods {
Random,
Consistent,
Timestamp,

View File

@ -104,6 +104,6 @@ export default definePlugin({
stop() {
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); // clear status
ws.close(); // close WebSocket
ws?.close(); // close WebSocket
}
});

View File

@ -36,7 +36,8 @@ function Guilds(props: {
// @ts-expect-error
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props);
const scrollerProps = res.props.children?.props?.children?.[1]?.props;
// TODO: Make this better
const scrollerProps = res.props.children?.props?.children?.props?.children?.[1]?.props;
if (scrollerProps?.children) {
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
if (servers) scrollerProps.children = servers;

View File

@ -0,0 +1,100 @@
/*
* 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 { ScreenshareIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { openImageModal } from "@utils/discord";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
import { Channel, User } from "discord-types/general";
import { ApplicationStreamingStore, ApplicationStreamPreviewStore } from "./webpack/stores";
import { ApplicationStream, Stream } from "./webpack/types/stores";
export interface UserContextProps {
channel: Channel,
channelSelected: boolean,
className: string,
config: { context: string; };
context: string,
onHeightUpdate: Function,
position: string,
target: HTMLElement,
theme: string,
user: User;
}
export interface StreamContextProps {
appContext: string,
className: string,
config: { context: string; };
context: string,
exitFullscreen: Function,
onHeightUpdate: Function,
position: string,
target: HTMLElement,
stream: Stream,
theme: string,
}
export const handleViewPreview = async ({ guildId, channelId, ownerId }: ApplicationStream | Stream) => {
const previewUrl = await ApplicationStreamPreviewStore.getPreviewURL(guildId, channelId, ownerId);
if (!previewUrl) return;
openImageModal(previewUrl);
};
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => () => {
const stream = ApplicationStreamingStore.getAnyStreamForUser(userId);
if (!stream) return;
const streamPreviewItem = (
<Menu.MenuItem
label="View Stream Preview"
id="view-stream-preview"
icon={ScreenshareIcon}
action={() => stream && handleViewPreview(stream)}
disabled={!stream}
/>
);
children.push(<Menu.MenuSeparator />, streamPreviewItem);
};
export const streamContextPatch: NavContextMenuPatchCallback = (children, { stream }: StreamContextProps) => {
return addViewStreamContext(children, { userId: stream.ownerId });
};
export const userContextPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
return addViewStreamContext(children, { userId: user.id });
};
export default definePlugin({
name: "BiggerStreamPreview",
description: "This plugin allows you to enlarge stream previews",
authors: [Devs.phil],
start: () => {
addContextMenuPatch("user-context", userContextPatch);
addContextMenuPatch("stream-context", streamContextPatch);
},
stop: () => {
removeContextMenuPatch("user-context", userContextPatch);
removeContextMenuPatch("stream-context", streamContextPatch);
}
});

View File

@ -1,6 +1,6 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,20 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Badge } from "./Badge";
export interface Sender {
id : number,
discordID: string,
username: string,
profilePhoto: string,
badges: Badge[]
}
import { findStoreLazy } from "@webpack";
export interface Review {
comment: string,
id: number,
star: number,
sender: Sender,
timestamp: number
}
import * as t from "./types/stores";
export const ApplicationStreamPreviewStore: t.ApplicationStreamPreviewStore = findStoreLazy("ApplicationStreamPreviewStore");
export const ApplicationStreamingStore: t.ApplicationStreamingStore = findStoreLazy("ApplicationStreamingStore");

View File

@ -0,0 +1,77 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { FluxStore } from "@webpack/types";
export interface ApplicationStreamPreviewStore extends FluxStore {
getIsPreviewLoading: (guildId: string | bigint | null, channelId: string | bigint, ownerId: string | bigint) => boolean;
getPreviewURL: (guildId: string | bigint | null, channelId: string | bigint, ownerId: string | bigint) => Promise<string | null>;
getPreviewURLForStreamKey: (streamKey: string) => ReturnType<ApplicationStreamPreviewStore["getPreviewURL"]>;
}
export interface ApplicationStream {
streamType: string;
guildId: string | null;
channelId: string;
ownerId: string;
}
export interface Stream extends ApplicationStream {
state: string;
}
export interface RTCStream {
region: string,
streamKey: string,
viewerIds: string[];
}
export interface StreamMetadata {
id: string | null,
pid: number | null,
sourceName: string | null;
}
export interface StreamingStoreState {
activeStreams: [string, Stream][];
rtcStreams: { [key: string]: RTCStream; };
streamerActiveStreamMetadatas: { [key: string]: StreamMetadata | null; };
streamsByUserAndGuild: { [key: string]: { [key: string]: ApplicationStream; }; };
}
/**
* example how a stream key could look like: `call(type of connection):1116549917987192913(channelId):305238513941667851(ownerId)`
*/
export interface ApplicationStreamingStore extends FluxStore {
getActiveStreamForApplicationStream: (stream: ApplicationStream) => Stream | null;
getActiveStreamForStreamKey: (streamKey: string) => Stream | null;
getActiveStreamForUser: (userId: string | bigint, guildId?: string | bigint | null) => Stream | null;
getAllActiveStreams: () => Stream[];
getAllApplicationStreams: () => ApplicationStream[];
getAllApplicationStreamsForChannel: (channelId: string | bigint) => ApplicationStream[];
getAllActiveStreamsForChannel: (channelId: string | bigint) => Stream[];
getAnyStreamForUser: (userId: string | bigint) => Stream | ApplicationStream | null;
getStreamForUser: (userId: string | bigint, guildId?: string | bigint | null) => Stream | null;
getCurrentUserActiveStream: () => Stream | null;
getLastActiveStream: () => Stream | null;
getState: () => StreamingStoreState;
getRTCStream: (streamKey: string) => RTCStream | null;
getStreamerActiveStreamMetadata: () => StreamMetadata;
getViewerIds: (stream: ApplicationStream) => string[];
isSelfStreamHidden: (channelId: string | bigint | null) => boolean;
}

View File

@ -74,7 +74,7 @@ interface Activity {
flags: number;
}
enum ActivityType {
const enum ActivityType {
PLAYING = 0,
LISTENING = 2,
WATCHING = 3,

View File

@ -22,11 +22,12 @@ import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general";
import type { ReactNode } from "react";
import type { ReactElement, ReactNode } from "react";
const DRAFT_TYPE = 0;
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
@ -392,70 +393,137 @@ export default definePlugin({
});
},
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
if (content.length > 1 && !settings.store.transformCompoundSentence) return content;
trimContent(content: Array<any>) {
const firstContent = content[0];
if (typeof firstContent === "string") content[0] = firstContent.trimStart();
if (content[0] === "") content.shift();
const newContent: Array<any> = [];
const lastIndex = content.length - 1;
const lastContent = content[lastIndex];
if (typeof lastContent === "string") content[lastIndex] = lastContent.trimEnd();
if (content[lastIndex] === "") content.pop();
},
clearEmptyArrayItems(array: Array<any>) {
return array.filter(item => item != null);
},
ensureChildrenIsArray(child: ReactElement) {
if (!Array.isArray(child.props.children)) child.props.children = [child.props.children];
},
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
// If content has more than one child or it's a single ReactElement like a header or list
if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content;
let nextIndex = content.length;
for (const element of content) {
if (element.props?.trusted == null) {
newContent.push(element);
continue;
}
const transformLinkChild = (child: ReactElement) => {
if (settings.store.transformEmojis) {
const fakeNitroMatch = element.props.href.match(fakeNitroEmojiRegex);
const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex);
if (fakeNitroMatch) {
let url: URL | null = null;
try {
url = new URL(element.props.href);
url = new URL(child.props.href);
} catch { }
const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
newContent.push(Parser.defaultRules.customEmoji.react({
jumboable: !inline && content.length === 1,
return Parser.defaultRules.customEmoji.react({
jumboable: !inline && content.length === 1 && typeof content[0].type !== "string",
animated: fakeNitroMatch[2] === "gif",
emojiId: fakeNitroMatch[1],
name: emojiName,
fake: true
}, void 0, { key: String(nextIndex++) }));
continue;
}, void 0, { key: String(nextIndex++) });
}
}
if (settings.store.transformStickers) {
if (fakeNitroStickerRegex.test(element.props.href)) continue;
if (fakeNitroStickerRegex.test(child.props.href)) return null;
const gifMatch = element.props.href.match(fakeNitroGifStickerRegex);
const gifMatch = child.props.href.match(fakeNitroGifStickerRegex);
if (gifMatch) {
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
if (StickerStore.getStickerById(gifMatch[1])) continue;
if (StickerStore.getStickerById(gifMatch[1])) return null;
}
}
newContent.push(element);
return child;
};
const transformChild = (child: ReactElement) => {
if (child?.props?.trusted != null) return transformLinkChild(child);
if (child?.props?.children != null) {
if (!Array.isArray(child.props.children)) {
child.props.children = modifyChild(child.props.children);
return child;
}
child.props.children = modifyChildren(child.props.children);
if (child.props.children.length === 0) return null;
return child;
}
return child;
};
const modifyChild = (child: ReactElement) => {
const newChild = transformChild(child);
if (newChild?.type === "ul" || newChild?.type === "ol") {
this.ensureChildrenIsArray(newChild);
if (newChild.props.children.length === 0) return null;
let listHasAnItem = false;
for (const [index, child] of newChild.props.children.entries()) {
if (child == null) {
delete newChild.props.children[index];
continue;
}
this.ensureChildrenIsArray(child);
if (child.props.children.length > 0) listHasAnItem = true;
else delete newChild.props.children[index];
}
if (!listHasAnItem) return null;
newChild.props.children = this.clearEmptyArrayItems(newChild.props.children);
}
return newChild;
};
const modifyChildren = (children: Array<ReactElement>) => {
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
children = this.clearEmptyArrayItems(children);
this.trimContent(children);
return children;
};
try {
return modifyChildren(window._.cloneDeep(content));
} catch (err) {
new Logger("FakeNitro").error(err);
return content;
}
const firstContent = newContent[0];
if (typeof firstContent === "string") newContent[0] = firstContent.trimStart();
return newContent;
},
patchFakeNitroStickers(stickers: Array<any>, message: Message) {
const itemsToMaybePush: Array<string> = [];
const contentItems = message.content.split(/\s/);
if (contentItems.length === 1 && !settings.store.transformCompoundSentence) itemsToMaybePush.push(contentItems[0]);
else itemsToMaybePush.push(...contentItems);
if (settings.store.transformCompoundSentence) itemsToMaybePush.push(...contentItems);
else if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]);
itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url));
for (const item of itemsToMaybePush) {
if (!settings.store.transformCompoundSentence && !item.startsWith("http")) continue;
const imgMatch = item.match(fakeNitroStickerRegex);
if (imgMatch) {
let url: URL | null = null;
@ -492,10 +560,13 @@ export default definePlugin({
},
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
if (message.content.split(/\s/).length > 1 && !settings.store.transformCompoundSentence) return false;
const contentItems = message.content.split(/\s/);
if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false;
switch (embed.type) {
case "image": {
if (!settings.store.transformCompoundSentence && !contentItems.includes(embed.url!) && !contentItems.includes(embed.image!.proxyURL)) return false;
if (settings.store.transformEmojis) {
if (fakeNitroEmojiRegex.test(embed.url!)) return true;
}

View File

@ -1,61 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ApplicationCommandOptionType } from "@api/Commands";
import { Settings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
export default definePlugin({
name: "Fart2",
authors: [Devs.Animal],
description: "Enable farting v2, a slash command that allows you to perform or request that someone perform a little toot.",
dependencies: ["CommandsAPI"],
commands: [{
name: "fart",
description: "A simple command in which you may either request that a user do a little toot for you, or conduct one yourself.",
options: [
{
type: ApplicationCommandOptionType.USER,
name: "user",
description: "A Discord™ user of which you would humbly request a toot from.",
required: false
}
],
execute(args) {
const fart = new Audio("https://raw.githubusercontent.com/ItzOnlyAnimal/AliuPlugins/main/fart.mp3");
fart.volume = Settings.plugins.Fart2.volume;
fart.play();
return {
content: (args[0]) ? `<@${args[0].value}> fart` : "fart"
};
},
}],
options: {
volume: {
description: "how loud you wanna fart (aka volume)",
type: OptionType.SLIDER,
markers: makeRange(0, 1, 0.1),
default: 0.5,
stickToMarkers: false,
}
}
});

View File

@ -18,25 +18,15 @@
import { get, set } from "@api/DataStore";
import { addButton, removeButton } from "@api/MessagePopover";
import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { ChannelStore, FluxDispatcher } from "@webpack/common";
import { ChannelStore } from "@webpack/common";
let style: HTMLStyleElement;
const KEY = "HideAttachments_HiddenIds";
const ImageVisible = () => (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M5 21q-.825 0-1.413-.587Q3 19.825 3 19V5q0-.825.587-1.413Q4.175 3 5 3h14q.825 0 1.413.587Q21 4.175 21 5v14q0 .825-.587 1.413Q19.825 21 19 21Zm0-2h14V5H5v14Zm1-2h12l-3.75-5-3 4L9 13Zm-1 2V5v14Z" />
</svg>
);
const ImageInvisible = () => (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="m21 18.15-2-2V5H7.85l-2-2H19q.825 0 1.413.587Q21 4.175 21 5Zm-1.2 4.45L18.2 21H5q-.825 0-1.413-.587Q3 19.825 3 19V5.8L1.4 4.2l1.4-1.4 18.4 18.4ZM6 17l3-4 2.25 3 .825-1.1L5 7.825V19h11.175l-2-2Zm7.425-6.425ZM10.6 13.4Z" />
</svg>
);
let hiddenMessages: Set<string> = new Set();
const getHiddenMessages = () => get(KEY).then(set => {
hiddenMessages = set ?? new Set<string>();
@ -101,11 +91,5 @@ export default definePlugin({
await saveHiddenMessages(ids);
await this.buildCss();
// update is necessary to rerender the PopOver
FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",
message: { id }
});
}
});

View File

@ -24,7 +24,7 @@ import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Tooltip } from "webpack/common";
enum ActivitiesTypes {
const enum ActivitiesTypes {
Game,
Embedded
}
@ -45,7 +45,7 @@ function ToggleIconOff() {
className={RegisteredGamesClasses.overlayToggleIconOff}
height="24"
width="24"
viewBox="0 0 32 26"
viewBox="0 2.2 32 26"
aria-hidden={true}
role="img"
>
@ -77,7 +77,7 @@ function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
className={RegisteredGamesClasses.overlayToggleIconOn}
height="24"
width="24"
viewBox="0 0 32 26"
viewBox="0 2.2 32 26"
>
<path
className={forceWhite ? "" : RegisteredGamesClasses.fill}
@ -119,7 +119,7 @@ function ToggleActivityComponentWithBackground({ activity }: { activity: Ignored
return (
<div
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
style={{ padding: "0px 2px" }}
style={{ padding: "0px 2px", height: 28 }}
>
<ToggleActivityComponent activity={activity} forceWhite={true} />
</div>
@ -157,10 +157,16 @@ export default definePlugin({
},
{
find: ".overlayBadge",
replacement: {
match: /(?<=\(\)\.badgeContainer.+?(\i)\.name}\):null)/,
replace: (_, props) => `,$self.renderToggleActivityButton(${props})`
}
replacement: [
{
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i)\.name.+?null/,
replace: (m, props) => `[${m},$self.renderToggleActivityButton(${props})]`
},
{
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i\.application)\.name.+?null/,
replace: (m, props) => `${m},$self.renderToggleActivityButton(${props})`
}
]
},
{
find: '.displayName="LocalActivityStore"',

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
import { ELEMENT_ID } from "../constants";
@ -33,6 +34,8 @@ export interface MagnifierProps {
instance: any;
}
const cl = classNameFactory("vc-imgzoom-");
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => {
const [ready, setReady] = useState(false);
@ -156,7 +159,7 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
return (
<div
className="vc-imgzoom-lens"
className={cl("lens", { "nearest-neighbor": settings.store.nearestNeighbour, square: settings.store.square })}
style={{
opacity,
width: size.current + "px",

View File

@ -23,7 +23,7 @@ import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import { debounce } from "@utils/debounce";
import definePlugin, { OptionType } from "@utils/types";
import { Menu, React, ReactDOM } from "@webpack/common";
import { ContextMenu, Menu, React, ReactDOM } from "@webpack/common";
import type { Root } from "react-dom/client";
import { Magnifier, MagnifierProps } from "./components/Magnifier";
@ -50,6 +50,18 @@ export const settings = definePluginSettings({
default: true,
},
nearestNeighbour: {
type: OptionType.BOOLEAN,
description: "Use Nearest Neighbour Interpolation when scaling images",
default: false,
},
square: {
type: OptionType.BOOLEAN,
description: "Make the lens square",
default: false,
},
zoom: {
description: "Zoom of the lens",
type: OptionType.SLIDER,
@ -78,9 +90,17 @@ export const settings = definePluginSettings({
const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
children.push(
<Menu.MenuGroup id="image-zoom">
{/* thanks SpotifyControls */}
<Menu.MenuCheckboxItem
id="vc-square"
label="Square Lens"
checked={settings.store.square}
action={() => {
settings.store.square = !settings.store.square;
ContextMenu.close();
}}
/>
<Menu.MenuControlItem
id="zoom"
id="vc-zoom"
label="Zoom"
control={(props, ref) => (
<Menu.MenuSliderControl
@ -94,7 +114,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
)}
/>
<Menu.MenuControlItem
id="size"
id="vc-size"
label="Lens Size"
control={(props, ref) => (
<Menu.MenuSliderControl
@ -108,7 +128,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
)}
/>
<Menu.MenuControlItem
id="zoom-speed"
id="vc-zoom-speed"
label="Zoom Speed"
control={(props, ref) => (
<Menu.MenuSliderControl

View File

@ -11,6 +11,14 @@
pointer-events: none;
}
.vc-imgzoom-square {
border-radius: 0;
}
.vc-imgzoom-nearest-neighbor > img {
image-rendering: pixelated; /* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */
}
/* make the carousel take up less space so we can click the backdrop and exit out of it */
[class|="carouselModal"] {
height: fit-content;

View File

@ -132,7 +132,7 @@ export default definePlugin({
find: ".Messages.MESSAGE_EDITED,",
replacement: {
match: /var .,.,.=(.)\.className,.=.\.message,.=.\.children,.=.\.content,.=.\.onUpdate/gm,
replace: "try {$1 && $self.INV_REGEX.test($1.content[0]) ? $1.content.push($self.indicator()) : null } catch {};$&"
replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&"
}
},
{
@ -200,8 +200,11 @@ export default definePlugin({
},
});
if (urlCheck?.length)
message.embeds.push(await this.getEmbed(new URL(urlCheck[0])));
if (urlCheck?.length) {
const embed = await this.getEmbed(new URL(urlCheck[0]));
if (embed)
message.embeds.push(embed);
}
this.updateMessage(message);
},

View File

@ -63,12 +63,12 @@ interface TrackData {
}
// only relevant enum values
enum ActivityType {
const enum ActivityType {
PLAYING = 0,
LISTENING = 2,
}
enum ActivityFlag {
const enum ActivityFlag {
INSTANCE = 1 << 0,
}

View File

@ -67,7 +67,7 @@ export default definePlugin({
document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup);
this.onClick = addClickListener((msg, channel, event) => {
this.onClick = addClickListener((msg: any, channel, event) => {
const isMe = msg.author.id === UserStore.getCurrentUser().id;
if (!isDeletePressed) {
if (event.detail < 2) return;
@ -90,7 +90,16 @@ export default definePlugin({
});
}
} else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(MANAGE_CHANNELS, channel))) {
MessageActions.deleteMessage(channel.id, msg.id);
if (msg.deleted) {
FluxDispatcher.dispatch({
type: "MESSAGE_DELETE",
channelId: channel.id,
id: msg.id,
mlDeleted: true
});
} else {
MessageActions.deleteMessage(channel.id, msg.id);
}
event.preventDefault();
}
});

View File

@ -225,6 +225,8 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
} else {
const msg = { ...message } as any;
delete msg.embeds;
delete msg.interaction;
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
.then(m => m && FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",

View File

@ -1,4 +1,10 @@
.messagelogger-deleted :is(div, h1, h2, h3, p) {
/* Message content highlighting */
.messagelogger-deleted [class*="contents-"] > :is(div, h1, h2, h3, p) {
color: #f04747 !important;
}
/* Embed highlighting */
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
color: #f04747 !important;
}

View File

@ -147,6 +147,11 @@ export default definePlugin({
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by yourself",
default: false
},
ignoreUsers: {
type: OptionType.STRING,
description: "Comma-separated list of user IDs to ignore",
default: ""
}
},
@ -154,7 +159,7 @@ export default definePlugin({
try {
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
const { ignoreBots, ignoreSelf } = Settings.plugins.MessageLogger;
const { ignoreBots, ignoreSelf, ignoreUsers } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
function mutate(id: string) {
@ -165,7 +170,8 @@ export default definePlugin({
const shouldIgnore = data.mlDeleted ||
(msg.flags & EPHEMERAL) === EPHEMERAL ||
ignoreBots && msg.author?.bot ||
ignoreSelf && msg.author?.id === myId;
ignoreSelf && msg.author?.id === myId ||
ignoreUsers.includes(msg.author?.id);
if (shouldIgnore) {
cache = cache.remove(id);

View File

@ -2,16 +2,29 @@
display: none;
}
.messagelogger-deleted :is(video, .emoji, [data-type="sticker"]),
.messagelogger-deleted .messagelogger-deleted-attachment,
.messagelogger-deleted div iframe {
.messagelogger-deleted
:is(
video,
.emoji,
[data-type="sticker"],
iframe,
.messagelogger-deleted-attachment,
[class|="inlineMediaEmbed"]
) {
filter: grayscale(1) !important;
transition: 150ms filter ease-in-out;
}
.messagelogger-deleted:hover :is(video, .emoji, [data-type="sticker"]),
.messagelogger-deleted .messagelogger-deleted-attachment:hover,
.messagelogger-deleted iframe:hover {
.messagelogger-deleted
:is(
video,
.emoji,
[data-type="sticker"],
iframe,
.messagelogger-deleted-attachment,
[class|="inlineMediaEmbed"]
):hover,
.messagelogger-deleted {
filter: grayscale(0) !important;
}

View File

@ -21,7 +21,7 @@ import { makeRange } from "@components/PluginSettings/components/SettingSliderCo
import { Devs } from "@utils/constants";
import { sleep } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { SelectedChannelStore, UserStore } from "@webpack/common";
import { RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common";
import { Message, ReactionEmoji } from "discord-types/general";
interface IMessageCreate {
@ -37,6 +37,7 @@ interface IReactionAdd {
optimistic: boolean;
channelId: string;
messageId: string;
messageAuthorId: string;
userId: "195136840355807232";
emoji: ReactionEmoji;
}
@ -71,6 +72,11 @@ const settings = definePluginSettings({
description: "Ignore bots",
type: OptionType.BOOLEAN,
default: true
},
ignoreBlocked: {
description: "Ignore blocked users",
type: OptionType.BOOLEAN,
default: true
}
});
@ -85,6 +91,7 @@ export default definePlugin({
if (optimistic || type !== "MESSAGE_CREATE") return;
if (message.state === "SENDING") return;
if (settings.store.ignoreBots && message.author?.bot) return;
if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(message.author?.id)) return;
if (!message.content) return;
if (channelId !== SelectedChannelStore.getChannelId()) return;
@ -96,9 +103,10 @@ export default definePlugin({
}
},
MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, emoji }: IReactionAdd) {
MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, messageAuthorId, emoji }: IReactionAdd) {
if (optimistic || type !== "MESSAGE_REACTION_ADD") return;
if (settings.store.ignoreBots && UserStore.getUser(userId)?.bot) return;
if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(messageAuthorId)) return;
if (channelId !== SelectedChannelStore.getChannelId()) return;
const name = emoji.name.toLowerCase();

View File

@ -18,37 +18,8 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { ModalContent, ModalFooter, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findStoreLazy } from "@webpack";
import { Button, Text } from "@webpack/common";
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
function NoDMNotificationsModal({ modalProps }: { modalProps: ModalProps; }) {
return (
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<ModalContent>
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center", "alignItems": "center", textAlign: "center", height: "100%", padding: "8px 0", gap: "16px" }}>
<Text variant="text-lg/semibold">You seem to have been affected by a bug that caused DM notifications to be muted and break if you used the MuteNewGuild plugin.</Text>
<Text variant="text-lg/semibold">If you haven't received any notifications for private messages, this is why. This issue is now fixed, so they should work again. Please verify, and in case they are still broken, ask for help in the Vencord support channel!</Text>
<Text variant="text-lg/semibold">We're very sorry for any inconvenience caused by this issue :(</Text>
</div>
</ModalContent>
<ModalFooter>
<div style={{ display: "flex", justifyContent: "center", width: "100%" }}>
<Button
onClick={modalProps.onClose}
size={Button.Sizes.MEDIUM}
color={Button.Colors.BRAND}
>
Understood!
</Button>
</div>
</ModalFooter>
</ModalRoot>
);
}
import { findByProps } from "@webpack";
const settings = definePluginSettings({
guild: {
@ -92,15 +63,5 @@ export default definePlugin({
suppress_roles: settings.store.role
}
);
},
start() {
const [isMuted, isEveryoneSupressed, isRolesSupressed] = [UserGuildSettingsStore.isMuted(null), UserGuildSettingsStore.isSuppressEveryoneEnabled(null), UserGuildSettingsStore.isSuppressRolesEnabled(null)];
if (isMuted || isEveryoneSupressed || isRolesSupressed) {
findByProps("updateGuildNotificationSettings").updateGuildNotificationSettings(null, { muted: false, suppress_everyone: false, suppress_roles: false });
openModal(modalProps => <NoDMNotificationsModal modalProps={modalProps} />);
}
}
});

View File

@ -0,0 +1,104 @@
/*
* 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 { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common";
import { Channel, User } from "discord-types/general";
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
const AvatarUtils = findByPropsLazy("getChannelIconURL");
const UserUtils = findByPropsLazy("getGlobalName");
const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds");
const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon");
function getGroupDMName(channel: Channel) {
return channel.name ||
channel.recipients
.map(UserStore.getUser)
.filter(isNonNullish)
.map(c => RelationshipStore.getNickname(c.id) || UserUtils.getName(c))
.join(", ");
}
export default definePlugin({
name: "MutualGroupDMs",
description: "Shows mutual group dms in profiles",
authors: [Devs.amia],
patches: [
{
find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded
replacement: [
{
match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/,
replace: '$1?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),'
},
{
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);"
}
]
}
],
renderMutualGDMs(user: User, onClose: () => void) {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {
onClose();
SelectedChannelActionCreators.selectPrivateChannel(c.id);
}}
>
<Avatar
src={AvatarUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
>
</Avatar>
<div className={ProfileListClasses.listRowContent}>
<div className={ProfileListClasses.listName}>{getGroupDMName(c)}</div>
<div className={GuildLabelClasses.guildNick}>{c.recipients.length + 1} Members</div>
</div>
</Clickable>
));
return (
<ScrollerThin
className={ProfileListClasses.listScroller}
fade={true}
onClose={onClose}
>
{entries.length > 0
? entries
: (
<div className={ProfileListClasses.empty}>
<div className={ProfileListClasses.emptyIconFriends}></div>
<div className={ProfileListClasses.emptyText}>No group dms in common</div>
</div>
)
}
</ScrollerThin>
);
}
});

View File

@ -0,0 +1,97 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
const MessageRequestStore = findByPropsLazy("getMessageRequestsCount");
const settings = definePluginSettings({
hideFriendRequestsCount: {
type: OptionType.BOOLEAN,
description: "Hide incoming friend requests count",
default: true,
restartNeeded: true
},
hideMessageRequestsCount: {
type: OptionType.BOOLEAN,
description: "Hide message requests count",
default: true,
restartNeeded: true
},
hidePremiumOffersCount: {
type: OptionType.BOOLEAN,
description: "Hide nitro offers count",
default: true,
restartNeeded: true
}
});
export default definePlugin({
name: "NoPendingCount",
description: "Removes the ping count of incoming friend requests, message requests, and nitro offers.",
authors: [Devs.amia],
settings: settings,
// Functions used to determine the top left count indicator can be found in the single module that calls getUnacknowledgedOffers(...)
// or by searching for "showProgressBadge:"
patches: [
{
find: ".getPendingCount=",
predicate: () => settings.store.hideFriendRequestsCount,
replacement: {
match: /(?<=\.getPendingCount=function\(\)\{)/,
replace: "return 0;"
}
},
{
find: ".getMessageRequestsCount=",
predicate: () => settings.store.hideMessageRequestsCount,
replacement: {
match: /(?<=\.getMessageRequestsCount=function\(\)\{)/,
replace: "return 0;"
}
},
// This prevents the Message Requests tab from always hiding due to the previous patch (and is compatible with spam requests)
// In short, only the red badge is hidden. Button visibility behavior isn't changed.
{
find: ".getSpamChannelsCount(),",
predicate: () => settings.store.hideMessageRequestsCount,
replacement: {
match: /(?<=getSpamChannelsCount\(\),\i=)\i\.getMessageRequestsCount\(\)/,
replace: "$self.getRealMessageRequestCount()"
}
},
{
find: "showProgressBadge:",
predicate: () => settings.store.hidePremiumOffersCount,
replacement: {
match: /\(function\(\){return \i\.\i\.getUnacknowledgedOffers\(\i\)\.length}\)/,
replace: "(function(){return 0})"
}
}
],
getRealMessageRequestCount() {
return MessageRequestStore.getMessageRequestChannelIds().size;
}
});

View File

@ -0,0 +1,53 @@
/*
* 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: "NoProfileThemes",
description: "Completely removes Nitro profile themes",
authors: [Devs.TheKodeToad],
patches: [
{
find: ".NITRO_BANNER,",
replacement: {
// = isPremiumAtLeast(user.premiumType, TIER_2)
match: /=(?=\i\.\i\.isPremiumAtLeast\(null==(\i))/,
// = user.banner && isPremiumAtLeast(user.premiumType, TIER_2)
replace: "=$1?.banner&&"
}
},
{
find: "().avatarPositionPremiumNoBanner,default:",
replacement: {
// premiumUserWithoutBanner: foo().avatarPositionPremiumNoBanner, default: foo().avatarPositionNormal
match: /\.avatarPositionPremiumNoBanner(?=,default:\i\(\)\.(\i))/,
// premiumUserWithoutBanner: foo().avatarPositionNormal...
replace: ".$1"
}
},
{
find: ".hasThemeColors=function(){",
replacement: {
match: /(?<=key:"canUsePremiumProfileCustomization",get:function\(\){return)/,
replace: " false;"
}
}
]
});

147
src/plugins/openInApp.ts Normal file
View File

@ -0,0 +1,147 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { showToast, Toasts } from "@webpack/common";
import { MouseEvent } from "react";
const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user)\/(.+)(?:\?.+?)?$/;
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
const settings = definePluginSettings({
spotify: {
type: OptionType.BOOLEAN,
description: "Open Spotify links in the Spotify app",
default: true,
},
steam: {
type: OptionType.BOOLEAN,
description: "Open Steam links in the Steam app",
default: true,
},
epic: {
type: OptionType.BOOLEAN,
description: "Open Epic Games links in the Epic Games Launcher",
default: true,
}
});
export default definePlugin({
name: "OpenInApp",
description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser",
authors: [Devs.Ven],
settings,
patches: [
{
find: '"MaskedLinkStore"',
replacement: {
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,200}\.trusted)/,
replace: "return $self.handleLink(...arguments).then(handled => handled || $1)"
}
},
// Make Spotify profile activity links open in app on web
{
find: "WEB_OPEN(",
predicate: () => !IS_DISCORD_DESKTOP && settings.store.spotify,
replacement: {
match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g,
replace: "true$1VencordNative.native.openExternal"
}
},
{
find: ".CONNECTED_ACCOUNT_VIEWED,",
replacement: {
match: /(?<=href:\i,onClick:function\(\)\{)(?=return \i=(\i)\.type,.{0,50}CONNECTED_ACCOUNT_VIEWED)/,
replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);"
}
}
],
async handleLink(data: { href: string; }, event: MouseEvent) {
if (!data) return false;
let url = data.href;
if (!IS_WEB && ShortUrlMatcher.test(url)) {
event.preventDefault();
// CORS jumpscare
url = await VencordNative.pluginHelpers.OpenInApp.resolveRedirect(url);
}
spotify: {
if (!settings.store.spotify) break spotify;
const match = SpotifyMatcher.exec(url);
if (!match) break spotify;
const [, type, id] = match;
VencordNative.native.openExternal(`spotify:${type}:${id}`);
event.preventDefault();
return true;
}
steam: {
if (!settings.store.steam) break steam;
if (!SteamMatcher.test(url)) break steam;
VencordNative.native.openExternal(`steam://openurl/${url}`);
event.preventDefault();
// Steam does not focus itself so show a toast so it's slightly less confusing
showToast("Opened link in Steam", Toasts.Type.SUCCESS);
return true;
}
epic: {
if (!settings.store.epic) break epic;
const match = EpicMatcher.exec(url);
if (!match) break epic;
VencordNative.native.openExternal(`com.epicgames.launcher://store/${match[1]}`);
event.preventDefault();
return true;
}
// in case short url didn't end up being something we can handle
if (event.defaultPrevented) {
window.open(url, "_blank");
return true;
}
return false;
},
handleAccountView(event: { preventDefault(): void; }, platformType: string, userId: string) {
if (platformType === "spotify" && settings.store.spotify) {
VencordNative.native.openExternal(`spotify:user:${userId}`);
event.preventDefault();
} else if (platformType === "steam" && settings.store.steam) {
VencordNative.native.openExternal(`steam://openurl/https://steamcommunity.com/profiles/${userId}`);
showToast("Opened link in Steam", Toasts.Type.SUCCESS);
event.preventDefault();
}
}
});

View File

@ -19,10 +19,12 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ContextMenu, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { settings } from "..";
import { cl, getPermissionDescription, getPermissionString } from "../utils";
import { PermissionAllowedIcon, PermissionDefaultIcon, PermissionDeniedIcon } from "./icons";
@ -108,7 +110,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
<div
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
onContextMenu={e => {
if (permission.type === PermissionType.Role)
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
ContextMenu.open(e, () => (
<RoleContextMenu
guild={guild}
@ -135,7 +137,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
permission.type === PermissionType.Role
? role?.name || "Unknown Role"
: permission.type === PermissionType.User
? user?.tag || "Unknown User"
? (user && getUniqueUsername(user)) || "Unknown User"
: (
<Flex style={{ gap: "0.2em", justifyItems: "center" }}>
@owner

View File

@ -17,10 +17,11 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader";
import { proxyLazy } from "@utils/lazy";
import { classes } from "@utils/misc";
import { filters, findBulk } from "@webpack";
import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore, useState } from "@webpack/common";
import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common";
import type { Guild, GuildMember } from "discord-types/general";
import { PermissionsSortOrder, settings } from "..";
@ -47,7 +48,6 @@ const Classes = proxyLazy(() => {
function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) {
const stns = settings.use(["permissionsSortOrder"]);
const [viewPermissions, setViewPermissions] = useState(settings.store.defaultPermissionsDropdownState);
const [rolePermissions, userPermissions] = useMemo(() => {
const userPermissions: UserPermissions = [];
@ -97,78 +97,40 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
const { root, role, roleRemoveButton, roleNameOverflow, roles, rolePill, rolePillBorder, roleCircle, roleName } = Classes;
return (
<div>
<div className={cl("userperms-title-container")}>
<Text className={cl("userperms-title")} variant="eyebrow">Permissions</Text>
<div className={cl("userperms-btns-container")}>
<Tooltip text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("userperms-sortorder-btn")}
onClick={() => {
stns.permissionsSortOrder = stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole;
}}
<ExpandableHeader
headerText="Permissions"
moreTooltipText="Role Details"
onMoreClick={() =>
openRolesAndUsersPermissionsModal(
rolePermissions,
guild,
guildMember.nick || UserStore.getUser(guildMember.userId).username
)
}
defaultState={settings.store.defaultPermissionsDropdownState}
buttons={[
(<Tooltip text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("userperms-sortorder-btn")}
onClick={() => {
stns.permissionsSortOrder = stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole;
}}
>
<svg
width="20"
height="20"
viewBox="0 96 960 960"
transform={stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
>
<svg
width="20"
height="20"
viewBox="0 96 960 960"
transform={stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
>
<path fill="var(--text-normal)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
</svg>
</button>
)}
</Tooltip>
<Tooltip text="Role Details">
{tooltipProps => (
<button
{...tooltipProps}
className={cl("userperms-permdetails-btn")}
onClick={() =>
openRolesAndUsersPermissionsModal(
rolePermissions,
guild,
guildMember.nick || UserStore.getUser(guildMember.userId).username
)
}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
</svg>
</button>
)}
</Tooltip>
<Tooltip text={viewPermissions ? "Hide Permissions" : "View Permissions"}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("userperms-toggleperms-btn")}
onClick={() => setViewPermissions(v => !v)}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
transform={viewPermissions ? "scale(1 -1)" : "scale(1 1)"}
>
<path fill="var(--text-normal)" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
</svg>
</button>
)}
</Tooltip>
</div>
</div>
{viewPermissions && userPermissions.length > 0 && (
<path fill="var(--text-normal)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
</svg>
</button>
)}
</Tooltip>)
]}>
{userPermissions.length > 0 && (
<div className={classes(root, roles)}>
{userPermissions.map(({ permission, roleColor }) => (
<div className={classes(role, rolePill, rolePillBorder)}>
@ -190,7 +152,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
))}
</div>
)}
</div>
</ExpandableHeader>
);
}

View File

@ -63,6 +63,7 @@
grid-area: list;
display: flex;
flex-direction: column;
gap: 2px;
border-right: 2px solid var(--background-modifier-active);
}
@ -77,6 +78,15 @@
padding: 8px 5px;
cursor: pointer;
width: 230px;
border-radius: 5px;
}
.vc-permviewer-perms-list-item:hover {
background-color: var(--background-modifier-hover);
}
.vc-permviewer-perms-list-item-active {
background-color: var(--background-modifier-selected);
}
.vc-permviewer-perms-list-item > div {
@ -85,11 +95,6 @@
overflow: hidden;
}
.vc-permviewer-perms-list-item-active {
background-color: var(--background-modifier-selected);
border-radius: 5px;
}
.vc-permviewer-perms-role-circle {
border-radius: 50%;
width: 12px;

View File

@ -52,7 +52,7 @@ export function CompactPronounsChatComponentWrapper({ message }: { message: Mess
}
function PronounsChatComponent({ message }: { message: Message; }) {
const result = useFormattedPronouns(message.author.id);
const [result] = useFormattedPronouns(message.author.id);
return result
? (
@ -64,7 +64,7 @@ function PronounsChatComponent({ message }: { message: Message; }) {
}
export function CompactPronounsChatComponent({ message }: { message: Message; }) {
const result = useFormattedPronouns(message.author.id);
const [result] = useFormattedPronouns(message.author.id);
return result
? (

View File

@ -26,6 +26,11 @@ import { CompactPronounsChatComponentWrapper, PronounsChatComponentWrapper } fro
import { useProfilePronouns } from "./pronoundbUtils";
import { settings } from "./settings";
const PRONOUN_TOOLTIP_PATCH = {
match: /text:(.{0,10}.Messages\.USER_PROFILE_PRONOUNS)(?=,)/,
replace: '$& + (typeof vcPronounSource !== "undefined" ? ` (${vcPronounSource})` : "")'
};
export default definePlugin({
name: "PronounDB",
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven],
@ -50,18 +55,24 @@ export default definePlugin({
// Patch the profile popout username header to use our pronoun hook instead of Discord's pronouns
{
find: ".userTagNoNickname",
replacement: {
match: /=(\i)\.pronouns/,
replace: "=$self.useProfilePronouns($1.user.id)"
}
replacement: [
{
match: /,(\i)=(\i)\.pronouns/,
replace: ",[$1,vcPronounSource]=$self.useProfilePronouns($2.user.id)"
},
PRONOUN_TOOLTIP_PATCH
]
},
// Patch the profile modal username header to use our pronoun hook instead of Discord's pronouns
{
find: ".USER_PROFILE_ACTIVITY",
replacement: {
match: /\).showPronouns/,
replace: ").showPronouns||true;const vcPronounce=$self.useProfilePronouns(arguments[0].user.id);if(arguments[0].displayProfile)arguments[0].displayProfile.pronouns=vcPronounce"
}
replacement: [
{
match: /getGlobalName\(\i\);(?<=displayProfile.{0,200})/,
replace: "$&const [vcPronounce,vcPronounSource]=$self.useProfilePronouns(arguments[0].user.id);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce;"
},
PRONOUN_TOOLTIP_PATCH
]
}
],

View File

@ -19,17 +19,29 @@
import { Settings } from "@api/Settings";
import { VENCORD_USER_AGENT } from "@utils/constants";
import { debounce } from "@utils/debounce";
import { getCurrentChannel } from "@utils/discord";
import { useAwaiter } from "@utils/react";
import { findStoreLazy } from "@webpack";
import { UserStore } from "@webpack/common";
import { settings } from "./settings";
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
const UserProfileStore = findStoreLazy("UserProfileStore");
type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""];
export const enum PronounsFormat {
Lowercase = "LOWERCASE",
Capitalized = "CAPITALIZED"
}
export const enum PronounSource {
PreferPDB,
PreferDiscord
}
// A map of cached pronouns so the same request isn't sent twice
const cache: Record<string, PronounCode> = {};
// A map of ids and callbacks that should be triggered on fetch
@ -46,46 +58,63 @@ const bulkFetch = debounce(async () => {
}
});
export function useFormattedPronouns(id: string): string | null {
function getDiscordPronouns(id: string) {
return (
UserProfileStore.getGuildMemberProfile(id, getCurrentChannel()?.guild_id)?.pronouns
|| UserProfileStore.getUserProfile(id)?.pronouns
);
}
export function useFormattedPronouns(id: string): PronounsWithSource {
// Discord is so stupid you can put tons of newlines in pronouns
const discordPronouns = getDiscordPronouns(id)?.trim().replace(NewLineRe, " ");
const [result] = useAwaiter(() => fetchPronouns(id), {
fallbackValue: getCachedPronouns(id),
onError: e => console.error("Fetching pronouns failed: ", e)
});
// If the result is present and not "unspecified", and there is a mapping for the code, then return the mappings
if (result && result !== "unspecified" && PronounMapping[result])
return formatPronouns(result);
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
return [discordPronouns, "Discord"];
return null;
if (result && result !== "unspecified")
return [formatPronouns(result), "PronounDB"];
return [discordPronouns, "Discord"];
}
export function useProfilePronouns(id: string) {
export function useProfilePronouns(id: string): PronounsWithSource {
const pronouns = useFormattedPronouns(id);
if (!settings.store.showInProfile) return null;
if (!settings.store.showSelf && id === UserStore.getCurrentUser().id) return null;
if (!settings.store.showInProfile) return EmptyPronouns;
if (!settings.store.showSelf && id === UserStore.getCurrentUser().id) return EmptyPronouns;
return pronouns;
}
const NewLineRe = /\n+/g;
// Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string): PronounCode | null {
return cache[id] ?? null;
export function getCachedPronouns(id: string): string | null {
const cached = cache[id];
if (cached && cached !== "unspecified") return cached;
return cached || null;
}
// 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<string> {
return new Promise(res => {
// If cached, return the cached pronouns
if (id in cache) res(getCachedPronouns(id)!);
const cached = getCachedPronouns(id);
if (cached) return res(cached);
// If there is already a request added, then just add this callback to it
else if (id in requestQueue) requestQueue[id].push(res);
if (id in requestQueue) return requestQueue[id].push(res);
// If not already added, then add it and call the debounced function to make sure the request gets executed
else {
requestQueue[id] = [res];
bulkFetch();
}
requestQueue[id] = [res];
bulkFetch();
});
}
@ -116,7 +145,7 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
}
}
export function formatPronouns(pronouns: PronounCode): string {
export function formatPronouns(pronouns: string): string {
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
// For capitalized pronouns, just return the mapping (it is by default capitalized)
if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns];

View File

@ -19,7 +19,7 @@
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
import { PronounsFormat } from "./pronoundbUtils";
import { PronounsFormat, PronounSource } from "./pronoundbUtils";
export const settings = definePluginSettings({
pronounsFormat: {
@ -37,6 +37,21 @@ export const settings = definePluginSettings({
}
]
},
pronounSource: {
type: OptionType.SELECT,
description: "Where to source pronouns from",
options: [
{
label: "Prefer PronounDB, fall back to Discord",
value: PronounSource.PreferPDB,
default: true
},
{
label: "Prefer Discord, fall back to PronounDB (might lead to inconsistency between pronouns in chat and profile)",
value: PronounSource.PreferDiscord
}
]
},
showSelf: {
type: OptionType.BOOLEAN,
description: "Enable or disable showing pronouns for the current user",

View File

@ -159,9 +159,12 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
return i === - 1 ? undefined : messages[messages.length - i - 1];
}
function shouldMention() {
function shouldMention(message) {
const { enabled, userList, shouldPingListed } = Settings.plugins.NoReplyMention;
const shouldPing = !enabled || (shouldPingListed === userList.includes(message.author.id));
switch (settings.store.shouldMention) {
case MentionOptions.NO_REPLY_MENTION_PLUGIN: return !Settings.plugins.NoReplyMention.enabled;
case MentionOptions.NO_REPLY_MENTION_PLUGIN: return shouldPing;
case MentionOptions.DISABLED: return false;
default: return true;
}
@ -184,7 +187,7 @@ function nextReply(isUp: boolean) {
type: "CREATE_PENDING_REPLY",
channel,
message,
shouldMention: shouldMention(),
shouldMention: shouldMention(message),
showMentionToggle: channel.guild_id !== null && message.author.id !== meId,
_isQuickReply: true
});

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getUniqueUsername, openUserProfile } from "@utils/discord";
import { UserUtils } from "@webpack/common";
import settings from "./settings";
@ -43,11 +44,19 @@ export async function onRelationshipRemove({ relationship: { type, id } }: Relat
switch (type) {
case RelationshipType.FRIEND:
if (settings.store.friends)
notify(`${user.tag} removed you as a friend.`, user.getAvatarURL(undefined, undefined, false));
notify(
`${getUniqueUsername(user)} removed you as a friend.`,
user.getAvatarURL(undefined, undefined, false),
() => openUserProfile(user.id)
);
break;
case RelationshipType.FRIEND_REQUEST:
if (settings.store.friendRequestCancels)
notify(`A friend request from ${user.tag} has been removed.`, user.getAvatarURL(undefined, undefined, false));
notify(
`A friend request from ${getUniqueUsername(user)} has been removed.`,
user.getAvatarURL(undefined, undefined, false),
() => openUserProfile(user.id)
);
break;
}
}

View File

@ -18,6 +18,7 @@
import { DataStore, Notices } from "@api/index";
import { showNotification } from "@api/Notifications";
import { getUniqueUsername, openUserProfile } from "@utils/discord";
import { ChannelStore, GuildMemberStore, GuildStore, RelationshipStore, UserStore, UserUtils } from "@webpack/common";
import settings from "./settings";
@ -69,7 +70,11 @@ export async function syncAndRunChecks() {
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));
notify(
`You are no longer friends with ${getUniqueUsername(user)}.`,
user.getAvatarURL(undefined, undefined, false),
() => openUserProfile(user.id)
);
}
}
@ -79,20 +84,25 @@ export async function syncAndRunChecks() {
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));
notify(
`Friend request from ${getUniqueUsername(user)} has been revoked.`,
user.getAvatarURL(undefined, undefined, false),
() => openUserProfile(user.id)
);
}
}
}
}
export function notify(text: string, icon?: string) {
export function notify(text: string, icon?: string, onClick?: () => void) {
if (settings.store.notices)
Notices.showNotice(text, "OK", () => Notices.popNotice());
showNotification({
title: "Relationship Notifier",
body: text,
icon
icon,
onClick
});
}

View File

@ -20,7 +20,7 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
const SpoilerClasses = findByPropsLazy("spoilerText");
const SpoilerClasses = findByPropsLazy("spoilerContent");
const MessagesClasses = findByPropsLazy("messagesWrapper", "messages");
export default definePlugin({
@ -43,14 +43,14 @@ export default definePlugin({
if (!ctrlKey) { return; }
const { spoilerText, hidden } = SpoilerClasses;
const { spoilerContent, hidden } = SpoilerClasses;
const { messagesWrapper } = MessagesClasses;
const parent = shiftKey
? document.querySelector(`div.${messagesWrapper}`)
: (target as HTMLSpanElement).parentElement;
for (const spoiler of parent!.querySelectorAll(`span.${spoilerText}.${hidden}`)) {
for (const spoiler of parent!.querySelectorAll(`span.${spoilerContent}.${hidden}`)) {
(spoiler as HTMLSpanElement).click();
}
}

View File

@ -17,28 +17,44 @@
*/
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import { findByProps } from "@webpack";
import { findByPropsLazy } from "@webpack";
import { Tooltip } from "@webpack/common";
export default LazyComponent(() => {
const { button, dangerous } = findByProps("button", "wrapper", "disabled", "separator");
const iconClasses = findByPropsLazy("button", "wrapper", "disabled", "separator");
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>
export function DeleteButton({ onClick }: { onClick(): void; }) {
return (
<Tooltip text="Delete Review">
{props => (
<div
{...props}
className={classes(iconClasses.button, iconClasses.dangerous)}
onClick={onClick}
>
<svg width="16" height="16" viewBox="0 0 20 20">
<path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z" />
<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" />
</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>
)}
</Tooltip>
);
}
export function ReportButton({ onClick }: { onClick(): void; }) {
return (
<Tooltip text="Report Review">
{props => (
<div
{...props}
className={iconClasses.button}
onClick={onClick}
>
<svg 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" />
</svg>
</div>
);
};
});
)}
</Tooltip>
);
}

View File

@ -18,7 +18,8 @@
import { MaskedLinkStore, Tooltip } from "@webpack/common";
import { Badge } from "../entities/Badge";
import { Badge } from "../entities";
import { cl } from "../utils";
export default function ReviewBadge(badge: Badge) {
return (
@ -26,13 +27,13 @@ export default function ReviewBadge(badge: Badge) {
text={badge.name}>
{({ onMouseEnter, onMouseLeave }) => (
<img
className={cl("badge")}
width="24px"
height="24px"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
src={badge.icon}
alt={badge.description}
style={{ verticalAlign: "middle", marginLeft: "4px" }}
onClick={() =>
MaskedLinkStore.openUntrustedLink({
href: badge.redirectURL,

View File

@ -16,16 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack";
import { Alerts, moment, Timestamp, 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 { Review, ReviewType } from "../entities";
import { deleteReview, reportReview } from "../reviewDbApi";
import { settings } from "../settings";
import { canDeleteReview, cl, showToast } from "../utils";
import { DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge";
export default LazyComponent(() => {
@ -36,18 +37,20 @@ export default LazyComponent(() => {
{ container, isHeader },
{ avatar, clickable, username, messageContent, wrapper, cozy },
buttonClasses,
botTag
] = findBulk(
p("cozyMessage"),
p("container", "isHeader"),
p("avatar", "zalgo"),
p("button", "wrapper", "selected"),
p("botTag")
);
const dateFormat = new Intl.DateTimeFormat();
return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) {
function openModal() {
openUserProfileModal(review.sender.discordID);
openUserProfile(review.sender.discordID);
}
function delReview() {
@ -79,21 +82,21 @@ export default LazyComponent(() => {
}
return (
<div className={classes(cozyMessage, wrapper, message, groupStart, cozy, "user-review")} style={
<div className={classes(cozyMessage, wrapper, message, groupStart, cozy, cl("review"))} style={
{
marginLeft: "0px",
paddingLeft: "52px",
paddingLeft: "52px", // wth is this
paddingRight: "16px"
}
}>
<div>
<img
className={classes(avatar, clickable)}
onClick={openModal}
src={review.sender.profilePhoto || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
style={{ left: "0px" }}
/>
<img
className={classes(avatar, clickable)}
onClick={openModal}
src={review.sender.profilePhoto || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
style={{ left: "0px" }}
/>
<div style={{ display: "inline-flex", justifyContent: "center", alignItems: "center" }}>
<span
className={classes(clickable, username)}
style={{ color: "var(--channels-default)", fontSize: "14px" }}
@ -101,32 +104,45 @@ export default LazyComponent(() => {
>
{review.sender.username}
</span>
{review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
{
!Settings.plugins.ReviewDB.hideTimestamps && (
<Timestamp timestamp={moment(review.timestamp * 1000)} >
{dateFormat.format(review.timestamp * 1000)}
</Timestamp>)
}
{review.type === ReviewType.System && (
<span
className={classes(botTag.botTagVerified, botTag.botTagRegular, botTag.botTag, botTag.px, botTag.rem)}
style={{ marginLeft: "4px" }}>
<span className={botTag.botText}>
System
</span>
</span>
)}
</div>
{review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
<p
className={classes(messageContent)}
style={{ fontSize: 15, marginTop: 4, color: "var(--text-normal)" }}
>
{review.comment}
</p>
{
!settings.store.hideTimestamps && review.type !== ReviewType.System && (
<Timestamp timestamp={moment(review.timestamp * 1000)} >
{dateFormat.format(review.timestamp * 1000)}
</Timestamp>)
}
<p
className={classes(messageContent)}
style={{ fontSize: 15, marginTop: 4, color: "var(--text-normal)" }}
>
{review.comment}
</p>
{review.id !== 0 && (
<div className={classes(container, isHeader, buttons)} style={{
padding: "0px",
}}>
<div className={buttonClasses.wrapper} >
<MessageButton type="report" callback={reportRev} />
<ReportButton onClick={reportRev} />
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
<MessageButton type="delete" callback={delReview} />
<DeleteButton onClick={delReview} />
)}
</div>
</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,104 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useForceUpdater } from "@utils/react";
import { Paginator, Text, useRef, useState } from "@webpack/common";
import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings";
import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent";
import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: string; name: string; }) {
const [data, setData] = useState<Response>();
const [signal, refetch] = useForceUpdater(true);
const [page, setPage] = useState(1);
const ref = useRef<HTMLDivElement>(null);
const reviewCount = data?.reviewCount;
const ownReview = data?.reviews.find(r => r.sender.discordID === settings.store.user?.discordID);
return (
<ErrorBoundary>
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-lg/semibold" className={cl("modal-header")}>
{name}'s Reviews
{!!reviewCount && <span> ({reviewCount} Reviews)</span>}
</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent scrollerRef={ref}>
<div className={cl("modal-reviews")}>
<ReviewsView
discordId={discordId}
name={name}
page={page}
refetchSignal={signal}
onFetchReviews={setData}
scrollToTop={() => ref.current?.scrollTo({ top: 0, behavior: "smooth" })}
hideOwnReview
/>
</div>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<div>
{ownReview && (
<ReviewComponent
refetch={refetch}
review={ownReview}
/>
)}
<ReviewsInputComponent
isAuthor={ownReview != null}
discordId={discordId}
name={name}
refetch={refetch}
/>
{!!reviewCount && (
<Paginator
currentPage={page}
maxVisiblePages={5}
pageSize={REVIEWS_PER_PAGE}
totalCount={reviewCount}
onPageChange={setPage}
/>
)}
</div>
</ModalFooter>
</ModalRoot>
</ErrorBoundary>
);
}
export function openReviewsModal(discordId: string, name: string) {
openModal(props => (
<Modal
modalProps={props}
discordId={discordId}
name={name}
/>
));
}

View File

@ -16,42 +16,113 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import { useAwaiter, useForceUpdater } from "@utils/react";
import { findByPropsLazy } from "@webpack";
import { Forms, React, Text, UserStore } from "@webpack/common";
import { Forms, React, UserStore } from "@webpack/common";
import type { KeyboardEvent } from "react";
import { addReview, getReviews } from "../Utils/ReviewDBAPI";
import { authorize, showToast } from "../Utils/Utils";
import { Review } from "../entities";
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings";
import { authorize, cl, showToast } from "../utils";
import ReviewComponent from "./ReviewComponent";
const Classes = findByPropsLazy("inputDefault", "editable");
export default function ReviewsView({ userId }: { userId: string; }) {
const { token } = Settings.plugins.ReviewDB;
const [refetchCount, setRefetchCount] = React.useState(0);
const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), {
fallbackValue: [],
deps: [refetchCount],
interface UserProps {
discordId: string;
name: string;
}
interface Props extends UserProps {
onFetchReviews(data: Response): void;
refetchSignal?: unknown;
showInput?: boolean;
page?: number;
scrollToTop?(): void;
hideOwnReview?: boolean;
}
export default function ReviewsView({
discordId,
name,
onFetchReviews,
refetchSignal,
scrollToTop,
page = 1,
showInput = false,
hideOwnReview = false,
}: Props) {
const [signal, refetch] = useForceUpdater(true);
const [reviewData] = useAwaiter(() => getReviews(discordId, (page - 1) * REVIEWS_PER_PAGE), {
fallbackValue: null,
deps: [refetchSignal, signal, page],
onSuccess: data => {
scrollToTop?.();
onFetchReviews(data!);
}
});
const username = UserStore.getUser(userId)?.username ?? "";
const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
if (!reviewData) return null;
if (isLoading) return null;
return (
<>
<ReviewList
refetch={refetch}
reviews={reviewData!.reviews}
hideOwnReview={hideOwnReview}
/>
{showInput && (
<ReviewsInputComponent
name={name}
discordId={discordId}
refetch={refetch}
isAuthor={reviewData!.reviews?.some(r => r.sender.discordID === UserStore.getCurrentUser().id)}
/>
)}
</>
);
}
function ReviewList({ refetch, reviews, hideOwnReview }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; }) {
const myId = UserStore.getCurrentUser().id;
return (
<div className={cl("view")}>
{reviews?.map(review =>
(review.sender.discordID !== myId || !hideOwnReview) &&
<ReviewComponent
key={review.id}
review={review}
refetch={refetch}
/>
)}
{reviews?.length === 0 && (
<Forms.FormText className={cl("placeholder")}>
Looks like nobody reviewed this user yet. You could be the first!
</Forms.FormText>
)}
</div>
);
}
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
const { token } = settings.store;
function onKeyPress({ key, target }: KeyboardEvent<HTMLTextAreaElement>) {
if (key === "Enter") {
addReview({
userid: userId,
userid: discordId,
comment: (target as HTMLInputElement).value,
star: -1
}).then(res => {
if (res?.success) {
(target as HTMLInputElement).value = ""; // clear the input
dirtyRefetch();
refetch();
} else if (res?.message) {
showToast(res.message);
}
@ -60,61 +131,27 @@ export default function ReviewsView({ userId }: { userId: string; }) {
}
return (
<div className="vc-reviewdb-view">
<Text
tag="h2"
variant="eyebrow"
style={{
marginBottom: "8px",
color: "var(--header-primary)"
}}
>
User Reviews
</Text>
{reviews?.map(review =>
<ReviewComponent
key={review.id}
review={review}
refetch={dirtyRefetch}
/>
)}
{reviews?.length === 0 && (
<Forms.FormText style={{ paddingRight: "12px", paddingTop: "0px", paddingLeft: "0px", paddingBottom: "4px", fontWeight: "bold", fontStyle: "italic" }}>
Looks like nobody reviewed this user yet. You could be the first!
</Forms.FormText>
)}
<textarea
className={classes(Classes.inputDefault, "enter-comment")}
onKeyDownCapture={e => {
if (e.key === "Enter") {
e.preventDefault(); // prevent newlines
}
}}
placeholder={
token
? (reviews?.some(r => r.sender.discordID === UserStore.getCurrentUser().id)
? `Update review for @${username}`
: `Review @${username}`)
: "You need to authorize to review users!"
<textarea
className={classes(Classes.inputDefault, "enter-comment", cl("input"))}
onKeyDownCapture={e => {
if (e.key === "Enter") {
e.preventDefault(); // prevent newlines
}
onKeyDown={onKeyPress}
onClick={() => {
if (!token) {
showToast("Opening authorization window...");
authorize();
}
}}
style={{
marginTop: "6px",
resize: "none",
marginBottom: "12px",
overflow: "hidden",
background: "transparent",
border: "1px solid var(--profile-message-input-border-color)",
fontSize: "14px",
}}
/>
</div>
}}
placeholder={
!token
? "You need to authorize to review users!"
: isAuthor
? `Update review for @${name}`
: `Review @${name}`
}
onKeyDown={onKeyPress}
onClick={() => {
if (!token) {
showToast("Opening authorization window...");
authorize();
}
}}
/>
);
}

View File

@ -0,0 +1,76 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const enum UserType {
Banned = -1,
Normal = 0,
Admin = 1
}
export const enum ReviewType {
User = 0,
Server = 1,
Support = 2,
System = 3
}
export interface Badge {
name: string;
description: string;
icon: string;
redirectURL: string;
type: number;
}
export interface BanInfo {
id: string;
discordID: string;
reviewID: number;
reviewContent: string;
banEndDate: number;
}
export interface ReviewDBUser {
ID: number;
discordID: string;
username: string;
profilePhoto: string;
clientMod: string;
warningCount: number;
badges: any[];
banInfo: BanInfo | null;
lastReviewID: number;
type: UserType;
}
export interface ReviewAuthor {
id: number,
discordID: string,
username: string,
profilePhoto: string,
badges: Badge[];
}
export interface Review {
comment: string,
id: number,
star: number,
sender: ReviewAuthor,
timestamp: number;
type?: ReviewType;
}

View File

@ -18,23 +18,40 @@
import "./style.css";
import { Settings } from "@api/Settings";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Alerts, Button } from "@webpack/common";
import { User } from "discord-types/general";
import definePlugin from "@utils/types";
import { Alerts, Menu, useState } from "@webpack/common";
import { Guild, User } from "discord-types/general";
import { openReviewsModal } from "./components/ReviewModal";
import ReviewsView from "./components/ReviewsView";
import { UserType } from "./entities/User";
import { getCurrentUserInfo } from "./Utils/ReviewDBAPI";
import { authorize, showToast } from "./Utils/Utils";
import { UserType } from "./entities";
import { getCurrentUserInfo } from "./reviewDbApi";
import { settings } from "./settings";
import { showToast } from "./utils";
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
children.push(
<Menu.MenuItem
label="View Reviews"
id="vc-rdb-server-reviews"
icon={OpenExternalIcon}
action={() => openReviewsModal(props.guild.id, props.guild.name)}
/>
);
};
export default definePlugin({
name: "ReviewDB",
description: "Review other users (Adds a new settings to profiles)",
authors: [Devs.mantikafasi, Devs.Ven],
settings,
patches: [
{
find: "disableBorderColor:!0",
@ -45,100 +62,86 @@ export default definePlugin({
}
],
options: {
authorize: {
type: OptionType.COMPONENT,
description: "Authorize with ReviewDB",
component: () => (
<Button onClick={authorize}>
Authorize with ReviewDB
</Button>
)
},
notifyReviews: {
type: OptionType.BOOLEAN,
description: "Notify about new reviews on startup",
default: true,
},
showWarning: {
type: OptionType.BOOLEAN,
description: "Display warning to be respectful at the top of the reviews list",
default: true,
},
hideTimestamps: {
type: OptionType.BOOLEAN,
description: "Hide timestamps on reviews",
default: false,
},
website: {
type: OptionType.COMPONENT,
description: "ReviewDB website",
component: () => (
<Button onClick={() => {
window.open("https://reviewdb.mantikafasi.dev");
}}>
ReviewDB website
</Button>
)
},
supportServer: {
type: OptionType.COMPONENT,
description: "ReviewDB Support Server",
component: () => (
<Button onClick={() => {
window.open("https://discord.gg/eWPBSbvznt");
}}>
ReviewDB Support Server
</Button>
)
},
},
async start() {
const settings = Settings.plugins.ReviewDB;
if (!settings.notifyReviews || !settings.token) return;
const s = settings.store;
const { token, lastReviewId, notifyReviews } = s;
if (!notifyReviews || !token) return;
setTimeout(async () => {
const user = await getCurrentUserInfo(settings.token);
if (settings.lastReviewId < user.lastReviewID) {
settings.lastReviewId = user.lastReviewID;
const user = await getCurrentUserInfo(token);
if (lastReviewId && lastReviewId < user.lastReviewID) {
s.lastReviewId = user.lastReviewID;
if (user.lastReviewID !== 0)
showToast("You have new reviews on your profile!");
}
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
if (user.banInfo) {
const endDate = new Date(user.banInfo.banEndDate);
if (endDate > new Date() && (settings.user?.banInfo?.banEndDate ?? 0) < endDate) {
if (endDate.getTime() > Date.now() && (s.user?.banInfo?.banEndDate ?? 0) < endDate.getTime()) {
Alerts.show({
title: "You have been banned from ReviewDB",
body: <>
<p>
You are banned from ReviewDB {(user.type === UserType.Banned) ? "permanently" : "until " + endDate.toLocaleString()}
</p>
<p>
Offending Review: {user.banInfo.reviewContent}
</p>
<p>
Continued offenses will result in a permanent ban.
</p>
</>,
body: (
<>
<p>
You are banned from ReviewDB {
user.type === UserType.Banned
? "permanently"
: "until " + endDate.toLocaleString()
}
</p>
{user.banInfo.reviewContent && (
<p>Offending Review: {user.banInfo.reviewContent}</p>
)}
<p>Continued offenses will result in a permanent ban.</p>
</>
),
cancelText: "Appeal",
confirmText: "Ok",
onCancel: () => {
window.open("https://forms.gle/Thj3rDYaMdKoMMuq6");
}
onCancel: () =>
VencordNative.native.openExternal(
"https://reviewdb.mantikafasi.dev/api/redirect?"
+ new URLSearchParams({
token: settings.store.token!,
page: "dashboard/appeal"
})
)
});
}
}
settings.user = user;
s.user = user;
}, 4000);
},
getReviewsComponent: (user: User) => (
<ErrorBoundary message="Failed to render Reviews">
<ReviewsView userId={user.id} />
</ErrorBoundary>
)
stop() {
removeContextMenuPatch("guild-header-popout", guildPopoutPatch);
},
getReviewsComponent: ErrorBoundary.wrap((user: User) => {
const [reviewCount, setReviewCount] = useState<number>();
return (
<ExpandableHeader
headerText="User Reviews"
onMoreClick={() => openReviewsModal(user.id, user.username)}
moreTooltipText={
reviewCount && reviewCount > 50
? `View all ${reviewCount} reviews`
: "Open Review Modal"
}
onDropDownClick={state => settings.store.reviewsDropdownState = !state}
defaultState={settings.store.reviewsDropdownState}
>
<ReviewsView
discordId={user.id}
name={user.username}
onFetchReviews={r => setReviewCount(r.reviewCount)}
showInput
/>
</ExpandableHeader>
);
}, { message: "Failed to render Reviews" })
});

View File

@ -16,54 +16,74 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { Review } from "../entities/Review";
import { ReviewDBUser } from "../entities/User";
import { authorize, showToast } from "./Utils";
import { Review, ReviewDBUser } from "./entities";
import { settings } from "./settings";
import { authorize, showToast } from "./utils";
const API_URL = "https://manti.vendicated.dev";
const getToken = () => Settings.plugins.ReviewDB.token;
export const REVIEWS_PER_PAGE = 50;
interface Response {
export interface Response {
success: boolean,
message: string;
reviews: Review[];
updated: boolean;
hasNextPage: boolean;
reviewCount: number;
}
const WarningFlag = 0b00000010;
export async function getReviews(id: string): Promise<Review[]> {
var flags = 0;
if (!Settings.plugins.ReviewDB.showWarning) flags |= WarningFlag;
const req = await fetch(API_URL + `/api/reviewdb/users/${id}/reviews?flags=${flags}`);
export async function getReviews(id: string, offset = 0): Promise<Response> {
let flags = 0;
if (!settings.store.showWarning) flags |= WarningFlag;
const params = new URLSearchParams({
flags: String(flags),
offset: String(offset)
});
const req = await fetch(`${API_URL}/api/reviewdb/users/${id}/reviews?${params}`);
const res = (req.status === 200)
? await req.json() as Response
: {
success: false,
message: "An Error occured while fetching reviews. Please try again later.",
reviews: [],
updated: false,
hasNextPage: false,
reviewCount: 0
};
const res = (req.status === 200) ? await req.json() as Response : { success: false, message: "An Error occured while fetching reviews. Please try again later.", reviews: [], updated: false };
if (!res.success) {
showToast(res.message);
return [
{
id: 0,
comment: "An Error occured while fetching reviews. Please try again later.",
star: 0,
timestamp: 0,
sender: {
return {
...res,
reviews: [
{
id: 0,
username: "Error",
profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128",
discordID: "0",
badges: []
comment: "An Error occured while fetching reviews. Please try again later.",
star: 0,
timestamp: 0,
sender: {
id: 0,
username: "Error",
profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128",
discordID: "0",
badges: []
}
}
}
];
]
};
}
return res.reviews;
return res;
}
export async function addReview(review: any): Promise<Response | null> {
review.token = getToken();
review.token = settings.store.token;
if (!review.token) {
showToast("Please authorize to add a review.");
@ -93,7 +113,7 @@ export function deleteReview(id: number): Promise<Response> {
Accept: "application/json",
}),
body: JSON.stringify({
token: getToken(),
token: settings.store.token,
reviewid: id
})
}).then(r => r.json());
@ -108,16 +128,16 @@ export async function reportReview(id: number) {
}),
body: JSON.stringify({
reviewid: id,
token: getToken()
token: settings.store.token
})
}).then(r => r.json()) as Response;
showToast(await res.message);
showToast(res.message);
}
export function getCurrentUserInfo(token: string): Promise<ReviewDBUser> {
return fetch(API_URL + "/api/reviewdb/users", {
body: JSON.stringify({ token }),
method: "POST",
})
.then(r => r.json());
}).then(r => r.json());
}

View File

@ -0,0 +1,82 @@
/*
* 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";
import { Button } from "@webpack/common";
import { ReviewDBUser } from "./entities";
import { authorize } from "./utils";
export const settings = definePluginSettings({
authorize: {
type: OptionType.COMPONENT,
description: "Authorize with ReviewDB",
component: () => (
<Button onClick={authorize}>
Authorize with ReviewDB
</Button>
)
},
notifyReviews: {
type: OptionType.BOOLEAN,
description: "Notify about new reviews on startup",
default: true,
},
showWarning: {
type: OptionType.BOOLEAN,
description: "Display warning to be respectful at the top of the reviews list",
default: true,
},
hideTimestamps: {
type: OptionType.BOOLEAN,
description: "Hide timestamps on reviews",
default: false,
},
website: {
type: OptionType.COMPONENT,
description: "ReviewDB website",
component: () => (
<Button onClick={() => {
let url = "https://reviewdb.mantikafasi.dev/";
if (settings.store.token)
url += "/api/redirect?token=" + encodeURIComponent(settings.store.token);
VencordNative.native.openExternal(url);
}}>
ReviewDB website
</Button>
)
},
supportServer: {
type: OptionType.COMPONENT,
description: "ReviewDB Support Server",
component: () => (
<Button onClick={() => {
VencordNative.native.openExternal("https://discord.gg/eWPBSbvznt");
}}>
ReviewDB Support Server
</Button>
)
}
}).withPrivateSettings<{
token?: string;
user?: ReviewDBUser;
lastReviewId?: number;
reviewsDropdownState?: boolean;
}>();

View File

@ -1,3 +1,51 @@
[class|="section"]:not([class|="lastSection"]) + .vc-reviewdb-view {
[class|="section"]:not([class|="lastSection"]) + .vc-rdb-view {
margin-top: 12px;
}
.vc-rdb-badge {
vertical-align: middle;
margin-left: 4px;
}
.vc-rdb-input {
margin-top: 6px;
margin-bottom: 12px;
resize: none;
overflow: hidden;
background: transparent;
border: 1px solid var(--profile-message-input-border-color);
font-size: 14px;
}
.vc-rdb-placeholder {
margin-bottom: 4px;
font-weight: bold;
font-style: italic;
color: var(--text-muted);
}
.vc-rdb-modal-footer {
padding: 0;
}
.vc-rdb-modal-footer > div {
width: 100%;
margin: 6px 16px;
}
.vc-rdb-modal-footer .vc-rdb-input {
margin-bottom: 0;
background: var(--input-background);
}
.vc-rdb-modal-footer [class|="pageControlContainer"] {
margin-top: 0;
}
.vc-rdb-modal-header {
flex-grow: 1;
}
.vc-rdb-modal-reviews {
margin-top: 16px;
}

View File

@ -16,25 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { FluxDispatcher, React, SelectedChannelStore, Toasts, UserUtils } from "@webpack/common";
import { React, Toasts } from "@webpack/common";
import { Review } from "../entities/Review";
import { UserType } from "../entities/User";
import { Review, UserType } from "./entities";
import { settings } from "./settings";
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 const cl = classNameFactory("vc-rdb-");
export function authorize(callback?: any) {
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
@ -48,16 +39,16 @@ export function authorize(callback?: any) {
permissions={0n}
clientId="915703782174752809"
cancelCompletesFlow={false}
callback={async (u: string) => {
callback={async (response: any) => {
try {
const url = new URL(u);
const url = new URL(response.location);
url.searchParams.append("clientMod", "vencord");
const res = await fetch(url, {
headers: new Headers({ Accept: "application/json" })
});
const { token, success } = await res.json();
if (success) {
Settings.plugins.ReviewDB.token = token;
settings.store.token = token;
showToast("Successfully logged in!");
callback?.();
} else if (res.status === 1) {
@ -82,8 +73,9 @@ export function showToast(text: string) {
});
}
export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
export function canDeleteReview(review: Review, userId: string) {
if (review.sender.discordID === userId || Settings.plugins.ReviewDB.user?.type === UserType.Admin) return true;
return (
review.sender.discordID === userId
|| settings.store.user?.type === UserType.Admin
);
}

View File

@ -24,7 +24,7 @@ import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { GuildStore, PresenceStore, RelationshipStore } from "@webpack/common";
enum IndicatorType {
const enum IndicatorType {
SERVER = 1 << 0,
FRIEND = 1 << 1,
BOTH = SERVER | FRIEND,

View File

@ -46,18 +46,18 @@ export type ShikiSpec = {
}) => Promise<IThemedToken[][]>;
};
export enum StyleSheets {
export const enum StyleSheets {
Main = "MAIN",
DevIcons = "DEVICONS",
}
export enum HljsSetting {
export const enum HljsSetting {
Never = "NEVER",
Secondary = "SECONDARY",
Primary = "PRIMARY",
Always = "ALWAYS",
}
export enum DeviconSetting {
export const enum DeviconSetting {
Disabled = "DISABLED",
Greyscale = "GREYSCALE",
Color = "COLOR"

View File

@ -147,6 +147,13 @@ function CompactConnectionComponent({ connection, theme }: { connection: Connect
className="vc-user-connection"
href={url}
target="_blank"
onClick={e => {
if (Vencord.Plugins.isPluginEnabled("OpenInApp")) {
const OpenInApp = Vencord.Plugins.plugins.OpenInApp as any as typeof import("../openInApp").default;
// handleLink will .preventDefault() if applicable
OpenInApp.handleLink(e.currentTarget, e);
}
}}
>
{img}
</a>

View File

@ -29,12 +29,12 @@ import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission
import { sortPermissionOverwrites } from "../../permissionsViewer/utils";
import { settings, VIEW_CHANNEL } from "..";
enum SortOrderTypes {
const enum SortOrderTypes {
LATEST_ACTIVITY = 0,
CREATION_DATE = 1
}
enum ForumLayoutTypes {
const enum ForumLayoutTypes {
DEFAULT = 0,
LIST = 1,
GRID = 2
@ -61,7 +61,7 @@ interface ExtendedChannel extends Channel {
availableTags?: Array<Tag>;
}
enum ChannelTypes {
const enum ChannelTypes {
GUILD_TEXT = 0,
GUILD_VOICE = 2,
GUILD_ANNOUNCEMENT = 5,
@ -69,12 +69,12 @@ enum ChannelTypes {
GUILD_FORUM = 15
}
enum VideoQualityModes {
const enum VideoQualityModes {
AUTO = 1,
FULL = 2
}
enum ChannelFlags {
const enum ChannelFlags {
PINNED = 1 << 1,
REQUIRE_TAG = 1 << 4
}

View File

@ -34,7 +34,7 @@ const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted
export const VIEW_CHANNEL = 1n << 10n;
const CONNECT = 1n << 20n;
enum ShowMode {
const enum ShowMode {
LockIcon,
HiddenIconWithMutedStyle
}
@ -107,8 +107,8 @@ export default definePlugin({
},
{
// Prevent Discord from trying to connect to hidden channels
match: /if\(!\i&&!\i(?=.{0,50}?selectVoiceChannel\((\i)\.id\))/,
replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})`
match: /(?=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\))/,
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
},
{
// Make Discord show inside the channel if clicking on a hidden or locked channel
@ -342,32 +342,27 @@ export default definePlugin({
]
},
{
find: "Guild voice channel without guild id.",
find: "useNotificationSettingsItem: channel cannot be undefined",
replacement: [
{
// Render our HiddenChannelLockScreen component instead of the main stage channel component
match: /Guild voice channel without guild id.+?children:(?<=(\i)\.getGuildId\(\).+?)(?=.{0,20}?}\)}function)/,
match: /"124px".+?children:(?<=var (\i)=\i\.channel.+?)(?=.{0,20}?}\)}function)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?$self.HiddenChannelLockScreen(${channel}):`
},
{
// Disable useless components for the HiddenChannelLockScreen of stage channels
match: /render(?!Header).{0,30}?:(?<=(\i)\.getGuildId\(\).+?Guild voice channel without guild id.+?)/g,
match: /render(?:BottomLeft|BottomCenter|BottomRight|ChatToasts):(?<=var (\i)=\i\.channel.+?)/g,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?null:`
},
// Prevent Discord from replacing our route if we aren't connected to the stage channel
{
match: /(?=!\i&&!\i&&!\i.{0,80}?(\i)\.getGuildId\(\).{0,50}?Guild voice channel without guild id)(?<=if\()/,
replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
},
{
// Disable gradients for the HiddenChannelLockScreen of stage channels
match: /Guild voice channel without guild id.+?disableGradients:(?<=(\i)\.getGuildId\(\).+?)/,
match: /"124px".+?disableGradients:(?<=(\i)\.getGuildId\(\).+?)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})||`
},
{
// Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels
match: /Guild voice channel without guild id.+?style:(?<=(\i)\.getGuildId\(\).+?)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?undefined:`
match: /"124px".+?style:(?<=(\i)\.getGuildId\(\).+?)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?void 0:`
},
{
// Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen
@ -376,7 +371,7 @@ export default definePlugin({
},
{
// Remove the open chat button for the HiddenChannelLockScreen
match: /"recents".+?null,(?=.+?channelId:(\i)\.id,showRequestToSpeakSidebar)/,
match: /"recents".+?&&(?=\(.+?channelId:(\i)\.id,showRequestToSpeakSidebar)/,
replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&`
}
],
@ -419,6 +414,14 @@ export default definePlugin({
match: /(?<=getChannels\(\i)(?=\))/,
replace: ",true"
}
},
{
find: '.displayName="NowPlayingViewStore"',
replacement: {
// Make active now voice states on hiddenl channels
match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/,
replace: "$1"
}
}
],

View File

@ -63,6 +63,7 @@ export default definePlugin({
settings,
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix }: UsernameProps) => {
if (message.interaction) return author?.nick;
try {
const { username } = message.author;
const { nick } = author;

View File

@ -23,6 +23,7 @@ import { Flex } from "@components/Flex";
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { debounce } from "@utils/debounce";
import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc";
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
@ -231,7 +232,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
id="view-cover"
label="View Album Cover"
// trolley
action={() => (Vencord.Plugins.plugins.ViewIcons as any).openImage(track.album.image.url)}
action={() => openImageModal(track.album.image.url)}
icon={ImageIcon}
/>
<Menu.MenuControlItem

View File

@ -89,7 +89,7 @@ export const SpotifyStore = proxyLazy(() => {
public isSettingPosition = false;
public openExternal(path: string) {
const url = Settings.plugins.SpotifyControls.useSpotifyUris
const url = Settings.plugins.SpotifyControls.useSpotifyUris || Vencord.Plugins.isPluginEnabled("OpenInApp")
? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":")
: "https://open.spotify.com" + path;

View File

@ -27,10 +27,16 @@ import { Alerts, Forms, UserStore } from "@webpack/common";
import gitHash from "~git-hash";
import plugins from "~plugins";
import settings from "./settings";
import settings from "./_core/settings";
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
const AllowedChannelIds = [
SUPPORT_CHANNEL_ID,
"1024286218801926184", // Vencord > #bot-spam
"1033680203433660458", // Vencord > #v
];
export default definePlugin({
name: "SupportHelper",
required: true,
@ -41,7 +47,7 @@ export default definePlugin({
commands: [{
name: "vencord-debug",
description: "Send Vencord Debug info",
predicate: ctx => ctx.channel.id === SUPPORT_CHANNEL_ID,
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
execute() {
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
@ -49,24 +55,36 @@ export default definePlugin({
if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;
if (IS_VENCORD_DESKTOP) return `Vencord Desktop v${VencordDesktopNative.app.getVersion()}`;
if ("armcord" in window) return `ArmCord v${window.armcord.version}`;
return `Web (${navigator.userAgent})`;
// @ts-expect-error
const name = typeof unsafeWindow !== "undefined" ? "UserScript" : "Web";
return `${name} (${navigator.userAgent})`;
})();
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
const enabledApiPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && isApiPlugin(p));
const info = {
Vencord: `v${VERSION}${gitHash}${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
"Discord Branch": RELEASE_CHANNEL,
Client: client,
Platform: window.navigator.platform,
Outdated: isOutdated,
OpenAsar: "openasar" in window,
};
const debugInfo = `
**Vencord Debug Info**
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
> Discord Branch: ${RELEASE_CHANNEL}
> Client: ${client}
> Platform: ${window.navigator.platform}
> Vencord Version: ${gitHash}${settings.additionalInfo}
> OpenAsar: ${"openasar" in window}
> Outdated: ${isOutdated}
> Enabled Plugins:
${makeCodeblock(Object.keys(plugins).filter(Vencord.Plugins.isPluginEnabled).join(", "))}
Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}):
${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))}
`;
return {
content: debugInfo.trim()
content: debugInfo.trim().replaceAll("```\n", "```")
};
}
}],

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