Compare commits

...

45 Commits

Author SHA1 Message Date
V
5d1d054b7e MessageLinkEmbeds: Use api 2023-06-24 16:55:57 +02:00
V
a11d5a9111 Add ComponentUpdaterAPI 2023-06-24 16:46:09 +02: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
99 changed files with 1351 additions and 410 deletions

View File

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

View File

@ -1,4 +1,7 @@
name: Sync to Codeberg name: Sync to Codeberg
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
on: on:
push: push:
workflow_dispatch: workflow_dispatch:
@ -7,6 +10,7 @@ on:
jobs: jobs:
codeberg: codeberg:
if: github.repository == 'Vendicated/Vencord'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

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

View File

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

View File

@ -12,5 +12,12 @@
"javascript.format.semicolons": "insert", "javascript.format.semicolons": "insert",
"typescript.format.semicolons": "insert", "typescript.format.semicolons": "insert",
"typescript.preferences.quoteStyle": "double", "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 # 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) ![](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)* *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 ## Vencord Desktop
> **Warning** > **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
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
[![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) [![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 ## Join our Support/Community Server
[![Vencord Discord Server](https://invidget.switchblade.xyz/D9uwnFnqmd?theme=dark)](https://discord.gg/D9uwnFnqmd) https://discord.gg/D9uwnFnqmd
## Disclaimer ## Disclaimer

View File

@ -41,12 +41,5 @@
"path": "modifyResponseHeaders.json" "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", "name": "vencord",
"private": "true", "private": "true",
"version": "1.2.6", "version": "1.2.9",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -24,7 +24,7 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"test": "pnpm 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", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit", "testTsc": "tsc --noEmit",
"uninject": "node scripts/runInstaller.mjs", "uninject": "node scripts/runInstaller.mjs",

View File

@ -19,11 +19,13 @@
import esbuild from "esbuild"; import esbuild from "esbuild";
import { commonOpts, globPlugins, isStandalone, watch } from "./common.mjs"; import { commonOpts, globPlugins, isStandalone, VERSION, watch } from "./common.mjs";
const defines = { const defines = {
IS_STANDALONE: isStandalone, 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 (defines.IS_STANDALONE === "false")
// If this is a local build (not standalone), optimise // 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 { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
import { join } from "path"; import { join } from "path";
// wtf is this assert syntax import { commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
import PackageJSON from "../../package.json" assert { type: "json" };
import { commonOpts, globPlugins, watch } from "./common.mjs";
/** /**
* @type {esbuild.BuildOptions} * @type {esbuild.BuildOptions}
@ -47,7 +45,9 @@ const commonOptions = {
IS_STANDALONE: "true", IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch), IS_DEV: JSON.stringify(watch),
IS_DISCORD_DESKTOP: "false", 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", outfile: "dist/Vencord.user.js",
banner: { 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: { footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local // 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)); let content = await readFile(join("browser", f));
if (f.startsWith("manifest")) { if (f.startsWith("manifest")) {
const json = JSON.parse(content.toString("utf-8")); const json = JSON.parse(content.toString("utf-8"));
json.version = PackageJSON.version; json.version = VERSION;
content = new TextEncoder().encode(JSON.stringify(json)); content = new TextEncoder().encode(JSON.stringify(json));
} }

View File

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

View File

@ -171,8 +171,8 @@ async function parseFile(fileName: string) {
throw fail("no default export called 'definePlugin' found"); throw fail("no default export called 'definePlugin' found");
} }
async function getEntryPoint(dirent: Dirent) { async function getEntryPoint(dir: string, dirent: Dirent) {
const base = join("./src/plugins", dirent.name); const base = join(dir, dirent.name);
if (!dirent.isDirectory()) return base; if (!dirent.isDirectory()) return base;
for (const name of ["index.ts", "index.tsx"]) { for (const name of ["index.ts", "index.tsx"]) {
@ -186,13 +186,23 @@ async function getEntryPoint(dirent: Dirent) {
throw new Error(`${dirent.name}: Couldn't find entry point`); 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 () => { (async () => {
parseDevs(); 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) { if (process.argv.length > 2) {
writeFileSync(process.argv[2], data); writeFileSync(process.argv[2], data);

View File

@ -22,7 +22,7 @@ import { ComponentType, HTMLProps } from "react";
import Plugins from "~plugins"; import Plugins from "~plugins";
export enum BadgePosition { export const enum BadgePosition {
START, START,
END END
} }
@ -79,7 +79,7 @@ export function _getBadges(args: BadgeUserArgs) {
: badges.push({ ...badge, ...args }); : 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); if (donorBadges) badges.unshift(...donorBadges);
return badges; return badges;

View File

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

View File

@ -0,0 +1,29 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { proxyLazy } from "@utils/lazy";
const p = proxyLazy<typeof import("plugins/_api/componentUpdater").default>(() => Vencord.Plugins.plugins.ComponentUpdaterAPI as any);
/**
* Rerender a specific message
* @param messageId The id of the message to rerender
*/
export function updateMessageComponent(messageId: string) {
p.forceUpdaters.get(messageId)?.();
}

View File

@ -20,7 +20,7 @@ import { Logger } from "@utils/Logger";
const logger = new Logger("ServerListAPI"); const logger = new Logger("ServerListAPI");
export enum ServerListRenderPosition { export const enum ServerListRenderPosition {
Above, Above,
In, 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) { export function definePluginSettings<
const definedSettings: DefinedSettings<D> = { Def extends SettingsDefinition,
Checks extends SettingsChecks<Def>,
PrivateSettings extends object = {}
>(def: Def, checks?: Checks) {
const definedSettings: DefinedSettings<Def, Checks, PrivateSettings> = {
get store() { get store() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized"); if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return Settings.plugins[definedSettings.pluginName] as any; return Settings.plugins[definedSettings.pluginName] as any;
@ -264,11 +268,11 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[] settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
).plugins[definedSettings.pluginName] as any, ).plugins[definedSettings.pluginName] as any,
def, def,
checks: checks ?? {}, checks: checks ?? {} as any,
pluginName: "", pluginName: "",
withPrivateSettings<T>() { withPrivateSettings<T extends object>() {
return this as DefinedSettings<D, C> & { store: T; }; return this as DefinedSettings<Def, Checks, T>;
} }
}; };

View File

@ -18,6 +18,7 @@
import * as $Badges from "./Badges"; import * as $Badges from "./Badges";
import * as $Commands from "./Commands"; import * as $Commands from "./Commands";
import * as $ComponentUpdater from "./ComponentUpdater";
import * as $ContextMenu from "./ContextMenu"; import * as $ContextMenu from "./ContextMenu";
import * as $DataStore from "./DataStore"; import * as $DataStore from "./DataStore";
import * as $MemberListDecorators from "./MemberListDecorators"; import * as $MemberListDecorators from "./MemberListDecorators";
@ -109,3 +110,8 @@ export const Notifications = $Notifications;
* An api allowing you to patch and add/remove items to/from context menus * An api allowing you to patch and add/remove items to/from context menus
*/ */
export const ContextMenu = $ContextMenu; export const ContextMenu = $ContextMenu;
/**
* An api allowing you to update/rerender components
*/
export const ComponentUpdater = $ComponentUpdater;

View File

@ -146,3 +146,47 @@ export function OwnerCrownIcon(props: IconProps) {
</Icon> </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 result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
const action = wasEnabled ? "stop" : "start";
if (!result) { if (!result) {
logger.error(`Failed to ${action} plugin ${plugin.name}`); settings.enabled = false;
showErrorToast(`Failed to ${action} plugin: ${plugin.name}`);
const msg = `Error while ${wasEnabled ? "stopping" : "starting"} plugin ${plugin.name}`;
logger.error(msg);
showErrorToast(msg);
return; return;
} }
@ -171,7 +173,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
); );
} }
enum SearchStatus { const enum SearchStatus {
ALL, ALL,
ENABLED, ENABLED,
DISABLED DISABLED

View File

@ -57,7 +57,7 @@
} }
.vc-text-selectable, .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 */ /* make text selectable, silly discord makes the entirety of settings not selectable */
user-select: text; 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_STANDALONE: boolean;
export var IS_DISCORD_DESKTOP: boolean; export var IS_DISCORD_DESKTOP: boolean;
export var IS_VENCORD_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 VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord"); export var Vencord: typeof import("./Vencord");

View File

@ -0,0 +1,51 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types";
import { useEffect } from "@webpack/common";
import { Channel, Message } from "discord-types/general";
const forceUpdaters = new Map<string, () => void>();
function useUpdater(data: { channel: Channel; message: Message; }) {
const forceUpdater = useForceUpdater();
useEffect(() => {
forceUpdaters.set(data.message.id, forceUpdater);
return () => void forceUpdaters.delete(data.message.id);
}, [data.message.id]);
}
export default definePlugin({
name: "ComponentUpdaterAPI",
description: "API to update / force rerender several components, such as messages",
authors: [Devs.Ven],
patches: [{
find: ".renderContentOnly;",
replacement: {
match: /=(\i)\.renderContentOnly;/,
replace: "$&$self.useUpdater($1);"
}
}],
useUpdater,
forceUpdaters
});

View File

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

View File

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

View File

@ -104,6 +104,6 @@ export default definePlugin({
stop() { stop() {
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); // clear status 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 // @ts-expect-error
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props); 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) { if (scrollerProps?.children) {
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS); const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
if (servers) scrollerProps.children = 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

@ -0,0 +1,25 @@
/*
* 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 { findStoreLazy } from "@webpack";
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; flags: number;
} }
enum ActivityType { const enum ActivityType {
PLAYING = 0, PLAYING = 0,
LISTENING = 2, LISTENING = 2,
WATCHING = 3, WATCHING = 3,

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,6 +18,7 @@
import { get, set } from "@api/DataStore"; import { get, set } from "@api/DataStore";
import { addButton, removeButton } from "@api/MessagePopover"; import { addButton, removeButton } from "@api/MessagePopover";
import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { ChannelStore } from "@webpack/common"; import { ChannelStore } from "@webpack/common";
@ -26,17 +27,6 @@ let style: HTMLStyleElement;
const KEY = "HideAttachments_HiddenIds"; 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(); let hiddenMessages: Set<string> = new Set();
const getHiddenMessages = () => get(KEY).then(set => { const getHiddenMessages = () => get(KEY).then(set => {
hiddenMessages = set ?? new Set<string>(); hiddenMessages = set ?? new Set<string>();

View File

@ -24,7 +24,7 @@ import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Tooltip } from "webpack/common"; import { Tooltip } from "webpack/common";
enum ActivitiesTypes { const enum ActivitiesTypes {
Game, Game,
Embedded Embedded
} }

View File

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

View File

@ -67,7 +67,7 @@ export default definePlugin({
document.addEventListener("keydown", keydown); document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup); 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; const isMe = msg.author.id === UserStore.getCurrentUser().id;
if (!isDeletePressed) { if (!isDeletePressed) {
if (event.detail < 2) return; if (event.detail < 2) return;
@ -90,7 +90,16 @@ export default definePlugin({
}); });
} }
} else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(MANAGE_CHANNELS, channel))) { } 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(); event.preventDefault();
} }
}); });

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { updateMessageComponent } from "@api/ComponentUpdater";
import { addAccessory } from "@api/MessageAccessories"; import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -28,7 +29,6 @@ import { find, findByCode, findByPropsLazy } from "@webpack";
import { import {
Button, Button,
ChannelStore, ChannelStore,
FluxDispatcher,
GuildStore, GuildStore,
MessageStore, MessageStore,
Parser, Parser,
@ -228,10 +228,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
delete msg.interaction; delete msg.interaction;
messageFetchQueue.push(() => fetchMessage(channelID, messageID) messageFetchQueue.push(() => fetchMessage(channelID, messageID)
.then(m => m && FluxDispatcher.dispatch({ .then(m => m && updateMessageComponent(message.id))
type: "MESSAGE_UPDATE",
message: msg
}))
); );
continue; continue;
} }

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; color: #f04747 !important;
} }

View File

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

View File

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

View File

@ -18,37 +18,8 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { ModalContent, ModalFooter, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findStoreLazy } from "@webpack"; import { findByProps } 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>
);
}
const settings = definePluginSettings({ const settings = definePluginSettings({
guild: { guild: {
@ -92,15 +63,5 @@ export default definePlugin({
suppress_roles: settings.store.role 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;"
}
}
]
});

View File

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

View File

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

View File

@ -52,7 +52,7 @@ export default definePlugin({
find: ".userTagNoNickname", find: ".userTagNoNickname",
replacement: { replacement: {
match: /=(\i)\.pronouns/, match: /=(\i)\.pronouns/,
replace: "=$self.useProfilePronouns($1.user.id)" replace: "=$self.useProfilePronouns($1.user.id,$1.pronouns)"
} }
}, },
// Patch the profile modal username header to use our pronoun hook instead of Discord's pronouns // Patch the profile modal username header to use our pronoun hook instead of Discord's pronouns
@ -60,7 +60,7 @@ export default definePlugin({
find: ".USER_PROFILE_ACTIVITY", find: ".USER_PROFILE_ACTIVITY",
replacement: { replacement: {
match: /\).showPronouns/, match: /\).showPronouns/,
replace: ").showPronouns||true;const vcPronounce=$self.useProfilePronouns(arguments[0].user.id);if(arguments[0].displayProfile)arguments[0].displayProfile.pronouns=vcPronounce" replace: ").showPronouns||true;const vcPronounce=$self.useProfilePronouns(arguments[0].user.id,arguments[0].displayProfile?.pronouns);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce"
} }
} }
], ],

View File

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

View File

@ -19,7 +19,7 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types"; import { OptionType } from "@utils/types";
import { PronounsFormat } from "./pronoundbUtils"; import { PronounsFormat, PronounSource } from "./pronoundbUtils";
export const settings = definePluginSettings({ export const settings = definePluginSettings({
pronounsFormat: { 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: { showSelf: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Enable or disable showing pronouns for the current user", description: "Enable or disable showing pronouns for the current user",

View File

@ -159,9 +159,12 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
return i === - 1 ? undefined : messages[messages.length - i - 1]; 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) { 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; case MentionOptions.DISABLED: return false;
default: return true; default: return true;
} }
@ -184,7 +187,7 @@ function nextReply(isUp: boolean) {
type: "CREATE_PENDING_REPLY", type: "CREATE_PENDING_REPLY",
channel, channel,
message, message,
shouldMention: shouldMention(), shouldMention: shouldMention(message),
showMentionToggle: channel.guild_id !== null && message.author.id !== meId, showMentionToggle: channel.guild_id !== null && message.author.id !== meId,
_isQuickReply: true _isQuickReply: true
}); });

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { getUniqueUsername, openUserProfile } from "@utils/discord";
import { UserUtils } from "@webpack/common"; import { UserUtils } from "@webpack/common";
import settings from "./settings"; import settings from "./settings";
@ -43,11 +44,19 @@ export async function onRelationshipRemove({ relationship: { type, id } }: Relat
switch (type) { switch (type) {
case RelationshipType.FRIEND: case RelationshipType.FRIEND:
if (settings.store.friends) 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; break;
case RelationshipType.FRIEND_REQUEST: case RelationshipType.FRIEND_REQUEST:
if (settings.store.friendRequestCancels) 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; break;
} }
} }

View File

@ -18,6 +18,7 @@
import { DataStore, Notices } from "@api/index"; import { DataStore, Notices } from "@api/index";
import { showNotification } from "@api/Notifications"; import { showNotification } from "@api/Notifications";
import { getUniqueUsername, openUserProfile } from "@utils/discord";
import { ChannelStore, GuildMemberStore, GuildStore, RelationshipStore, UserStore, UserUtils } from "@webpack/common"; import { ChannelStore, GuildMemberStore, GuildStore, RelationshipStore, UserStore, UserUtils } from "@webpack/common";
import settings from "./settings"; import settings from "./settings";
@ -69,7 +70,11 @@ export async function syncAndRunChecks() {
const user = await UserUtils.fetchUser(id).catch(() => void 0); const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user) 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); const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user) 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) if (settings.store.notices)
Notices.showNotice(text, "OK", () => Notices.popNotice()); Notices.showNotice(text, "OK", () => Notices.popNotice());
showNotification({ showNotification({
title: "Relationship Notifier", title: "Relationship Notifier",
body: text, body: text,
icon icon,
onClick
}); });
} }

View File

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

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react"; import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack"; import { filters, findBulk } from "@webpack";
@ -24,7 +25,7 @@ import { Alerts, moment, Timestamp, UserStore } from "@webpack/common";
import { Review, ReviewType } from "../entities"; import { Review, ReviewType } from "../entities";
import { deleteReview, reportReview } from "../reviewDbApi"; import { deleteReview, reportReview } from "../reviewDbApi";
import { settings } from "../settings"; import { settings } from "../settings";
import { canDeleteReview, cl, openUserProfileModal, showToast } from "../utils"; import { canDeleteReview, cl, showToast } from "../utils";
import { DeleteButton, ReportButton } from "./MessageButton"; import { DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge"; import ReviewBadge from "./ReviewBadge";
@ -49,7 +50,7 @@ export default LazyComponent(() => {
return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) { return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) {
function openModal() { function openModal() {
openUserProfileModal(review.sender.discordID); openUserProfile(review.sender.discordID);
} }
function delReview() { function delReview() {

View File

@ -79,8 +79,8 @@ export default definePlugin({
addContextMenuPatch("guild-header-popout", guildPopoutPatch); addContextMenuPatch("guild-header-popout", guildPopoutPatch);
if (user.banInfo) { if (user.banInfo) {
const endDate = new Date(user.banInfo.banEndDate).getTime(); const endDate = new Date(user.banInfo.banEndDate);
if (endDate > Date.now() && (s.user?.banInfo?.banEndDate ?? 0) < endDate) { if (endDate.getTime() > Date.now() && (s.user?.banInfo?.banEndDate ?? 0) < endDate.getTime()) {
Alerts.show({ Alerts.show({
title: "You have been banned from ReviewDB", title: "You have been banned from ReviewDB",
body: ( body: (

View File

@ -20,24 +20,13 @@ import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal"; import { openModal } from "@utils/modal";
import { findByProps } from "@webpack"; import { findByProps } from "@webpack";
import { FluxDispatcher, React, SelectedChannelStore, Toasts, UserUtils } from "@webpack/common"; import { React, Toasts } from "@webpack/common";
import { Review, UserType } from "./entities"; import { Review, UserType } from "./entities";
import { settings } from "./settings"; import { settings } from "./settings";
export const cl = classNameFactory("vc-rdb-"); export const cl = classNameFactory("vc-rdb-");
export async function openUserProfileModal(userId: string) {
await UserUtils.fetchUser(userId);
await FluxDispatcher.dispatch({
type: "USER_PROFILE_MODAL_OPEN",
userId,
channelId: SelectedChannelStore.getChannelId(),
analyticsLocation: "Explosive Hotel"
});
}
export function authorize(callback?: any) { export function authorize(callback?: any) {
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal"); const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
@ -50,9 +39,9 @@ export function authorize(callback?: any) {
permissions={0n} permissions={0n}
clientId="915703782174752809" clientId="915703782174752809"
cancelCompletesFlow={false} cancelCompletesFlow={false}
callback={async (u: string) => { callback={async (response: any) => {
try { try {
const url = new URL(u); const url = new URL(response.location);
url.searchParams.append("clientMod", "vencord"); url.searchParams.append("clientMod", "vencord");
const res = await fetch(url, { const res = await fetch(url, {
headers: new Headers({ Accept: "application/json" }) headers: new Headers({ Accept: "application/json" })

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted
export const VIEW_CHANNEL = 1n << 10n; export const VIEW_CHANNEL = 1n << 10n;
const CONNECT = 1n << 20n; const CONNECT = 1n << 20n;
enum ShowMode { const enum ShowMode {
LockIcon, LockIcon,
HiddenIconWithMutedStyle HiddenIconWithMutedStyle
} }
@ -107,8 +107,8 @@ export default definePlugin({
}, },
{ {
// Prevent Discord from trying to connect to hidden channels // Prevent Discord from trying to connect to hidden channels
match: /if\(!\i&&!\i(?=.{0,50}?selectVoiceChannel\((\i)\.id\))/, match: /(?=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\))/,
replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})` replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
}, },
{ {
// Make Discord show inside the channel if clicking on a hidden or locked channel // Make Discord show inside the channel if clicking on a hidden or locked channel
@ -419,6 +419,14 @@ export default definePlugin({
match: /(?<=getChannels\(\i)(?=\))/, match: /(?<=getChannels\(\i)(?=\))/,
replace: ",true" 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, settings,
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix }: UsernameProps) => { renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix }: UsernameProps) => {
if (message.interaction) return author?.nick;
try { try {
const { username } = message.author; const { username } = message.author;
const { nick } = author; const { nick } = author;

View File

@ -27,7 +27,7 @@ import { Alerts, Forms, UserStore } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import plugins from "~plugins"; import plugins from "~plugins";
import settings from "./settings"; import settings from "./_core/settings";
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss"; const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
@ -55,7 +55,10 @@ export default definePlugin({
if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`; if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;
if (IS_VENCORD_DESKTOP) return `Vencord Desktop v${VencordDesktopNative.app.getVersion()}`; if (IS_VENCORD_DESKTOP) return `Vencord Desktop v${VencordDesktopNative.app.getVersion()}`;
if ("armcord" in window) return `ArmCord v${window.armcord.version}`; 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 isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
@ -63,14 +66,18 @@ export default definePlugin({
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p)); 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 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 = ` const debugInfo = `
**Vencord Debug Info** **Vencord Debug Info**
>>> Discord Branch: ${RELEASE_CHANNEL} >>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
Client: ${client}
Platform: ${window.navigator.platform}
Vencord: ${gitHash}${settings.additionalInfo}
Outdated: ${isOutdated}
OpenAsar: ${"openasar" in window}
Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}): Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}):
${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))} ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))}

View File

@ -19,13 +19,13 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { openUserProfile } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { GuildMemberStore, React, RelationshipStore, SelectedChannelStore } from "@webpack/common"; import { GuildMemberStore, React, RelationshipStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
const Avatar = findByCodeLazy(".typingIndicatorRef", "svg"); const Avatar = findByCodeLazy(".typingIndicatorRef", "svg");
const openProfile = findByCodeLazy("friendToken", "USER_PROFILE_MODAL_OPEN");
const settings = definePluginSettings({ const settings = definePluginSettings({
showAvatars: { showAvatars: {
@ -64,15 +64,7 @@ const TypingUser = ErrorBoundary.wrap(function ({ user, guildId }: Props) {
<strong <strong
role="button" role="button"
onClick={() => { onClick={() => {
openProfile({ openUserProfile(user.id);
userId: user.id,
guildId,
channelId: SelectedChannelStore.getChannelId(),
analyticsLocation: {
page: guildId ? "Guild Channel" : "DM Channel",
section: "Profile Popout"
}
});
}} }}
style={{ style={{
display: "grid", display: "grid",

View File

@ -0,0 +1,67 @@
/*
* 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 { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
const EMBED_SUPPRESSED = 1 << 2;
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => {
const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0;
if (!isEmbedSuppressed && !embeds.length) return;
const hasEmbedPerms = channel.isPrivate() || !!(PermissionStore.getChannelPermissions({ id: channel.id }) & PermissionsBits.EMBED_LINKS);
if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return;
const menuGroup = findGroupChildrenByChildId("delete", children);
const deleteIndex = menuGroup?.findIndex(i => i?.props?.id === "delete");
if (!deleteIndex || !menuGroup) return;
menuGroup.splice(deleteIndex - 1, 0, (
<Menu.MenuItem
id="unsuppress-embeds"
key="unsuppress-embeds"
label={isEmbedSuppressed ? "Unsuppress Embeds" : "Suppress Embeds"}
color={isEmbedSuppressed ? undefined : "danger"}
icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}
action={() =>
RestAPI.patch({
url: `/channels/${channel.id}/messages/${messageId}`,
body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }
})
}
/>
));
};
export default definePlugin({
name: "UnsuppressEmbeds",
authors: [Devs.rad, Devs.HypedDomi],
description: "Allows you to unsuppress embeds in messages",
start() {
addContextMenuPatch("message", messageContextMenuPatch);
},
stop() {
removeContextMenuPatch("message", messageContextMenuPatch);
},
});

View File

@ -65,6 +65,7 @@ const replacements = [
["stupid", "baka"], ["stupid", "baka"],
["what", "nani"], ["what", "nani"],
["meow", "nya~"], ["meow", "nya~"],
["hello", "hewwo"],
]; ];
const settings = definePluginSettings({ const settings = definePluginSettings({

View File

@ -24,7 +24,7 @@ import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { UserStore, useState } from "@webpack/common"; import { UserStore, useState } from "@webpack/common";
import type { User } from "discord-types/general"; import type { User } from "discord-types/general";
import type { ComponentType } from "react"; import type { ComponentType, ReactNode } from "react";
const fetching = new Set<string>(); const fetching = new Set<string>();
const queue = new Queue(5); const queue = new Queue(5);
@ -36,7 +36,7 @@ interface MentionProps {
channelId?: string; channelId?: string;
content: any; content: any;
}; };
parse: (content: any, props: MentionProps["props"]) => string[]; parse: (content: any, props: MentionProps["props"]) => ReactNode;
props: { props: {
key: string; key: string;
formatInline: boolean; formatInline: boolean;
@ -72,7 +72,7 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
> >
<span <span
onMouseEnter={() => { onMouseEnter={() => {
const mention = children?.[0]; const mention = children?.[0]?.props?.children;
if (typeof mention !== "string") return; if (typeof mention !== "string") return;
const id = mention.match(/<@(\d+)>/)?.[1]; const id = mention.match(/<@(\d+)>/)?.[1];

View File

@ -71,7 +71,7 @@ function clean(str: string) {
function formatText(str: string, user: string, channel: string) { function formatText(str: string, user: string, channel: string) {
return str return str
.replaceAll("{{USER}}", clean(user) || user ? "Someone" : "") .replaceAll("{{USER}}", clean(user) || (user ? "Someone" : ""))
.replaceAll("{{CHANNEL}}", clean(channel) || "channel"); .replaceAll("{{CHANNEL}}", clean(channel) || "channel");
} }

View File

@ -20,15 +20,12 @@ import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatc
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { ImageIcon } from "@components/Icons"; import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { ModalRoot, ModalSize, openModal } from "@utils/modal"; import { openImageModal } from "@utils/discord";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { find, findByCode, findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { GuildMemberStore, Menu } from "@webpack/common"; import { GuildMemberStore, Menu } from "@webpack/common";
import type { Channel, Guild, User } from "discord-types/general"; import type { Channel, Guild, User } from "discord-types/general";
const ImageModal = LazyComponent(() => findByCode(".MEDIA_MODAL_CLOSE,"));
const MaskedLink = LazyComponent(() => find(m => m.type?.toString().includes("MASKED_LINK)")));
const BannerStore = findByPropsLazy("getGuildBannerURL"); const BannerStore = findByPropsLazy("getGuildBannerURL");
interface UserContextProps { interface UserContextProps {
@ -60,26 +57,29 @@ const settings = definePluginSettings({
value: "jpg", value: "jpg",
} }
] ]
},
imgSize: {
type: OptionType.SELECT,
description: "The image size to use",
options: ["128", "256", "512", "1024", "2048", "4096"].map(n => ({ label: n, value: n, default: n === "1024" }))
} }
}); });
function openImage(url: string) { function openImage(url: string) {
const format = url.startsWith("/") ? "png" : settings.store.format; const format = url.startsWith("/") ? "png" : settings.store.format;
const u = new URL(url, window.location.href); const u = new URL(url, window.location.href);
u.searchParams.set("size", "512"); u.searchParams.set("size", settings.store.imgSize);
u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`); u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`);
url = u.toString(); url = u.toString();
openModal(modalProps => ( u.searchParams.set("size", "4096");
<ModalRoot size={ModalSize.DYNAMIC} {...modalProps}> const originalUrl = u.toString();
<ImageModal
shouldAnimate={true} openImageModal(url, {
original={url} original: originalUrl,
src={url} height: 256
renderLinkComponent={MaskedLink} });
/>
</ModalRoot>
));
} }
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => { const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => {
@ -90,7 +90,7 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
<Menu.MenuItem <Menu.MenuItem
id="view-avatar" id="view-avatar"
label="View Avatar" label="View Avatar"
action={() => openImage(BannerStore.getUserAvatarURL(user, true, 512))} action={() => openImage(BannerStore.getUserAvatarURL(user, true))}
icon={ImageIcon} icon={ImageIcon}
/> />
{memberAvatar && ( {memberAvatar && (
@ -122,7 +122,6 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild: { id, icon
openImage(BannerStore.getGuildIconURL({ openImage(BannerStore.getGuildIconURL({
id, id,
icon, icon,
size: 512,
canAnimate: true canAnimate: true
})) }))
} }

View File

@ -17,13 +17,14 @@
*/ */
import { addButton, removeButton } from "@api/MessagePopover"; import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc"; import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Parser, Text } from "@webpack/common"; import { Button, ChannelStore, Forms, Parser, Text } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
@ -41,22 +42,12 @@ function sortObject<T extends object>(obj: T): T {
function cleanMessage(msg: Message) { function cleanMessage(msg: Message) {
const clone = sortObject(JSON.parse(JSON.stringify(msg))); const clone = sortObject(JSON.parse(JSON.stringify(msg)));
for (const key in clone.author) { for (const key of [
switch (key) { "email",
case "id": "phone",
case "username": "mfaEnabled",
case "usernameNormalized": "personalConnectionId"
case "discriminator": ]) delete clone.author[key];
case "avatar":
case "bot":
case "system":
case "publicFlags":
break;
default:
// phone number, email, etc
delete clone.author[key];
}
}
// message logger added properties // message logger added properties
const cloneAny = clone as any; const cloneAny = clone as any;
@ -116,26 +107,58 @@ function openViewRawModal(msg: Message) {
)); ));
} }
const settings = definePluginSettings({
clickMethod: {
description: "Change the button to view the raw content/data of any message.",
type: OptionType.SELECT,
options: [
{ label: "Left Click to view the raw content.", value: "Left", default: true },
{ label: "Right click to view the raw content.", value: "Right" }
]
}
});
export default definePlugin({ export default definePlugin({
name: "ViewRaw", name: "ViewRaw",
description: "Copy and view the raw content/data of any message.", description: "Copy and view the raw content/data of any message.",
authors: [Devs.KingFish, Devs.Ven], authors: [Devs.KingFish, Devs.Ven, Devs.rad],
dependencies: ["MessagePopoverAPI"], dependencies: ["MessagePopoverAPI"],
settings,
start() { start() {
addButton("ViewRaw", msg => { addButton("ViewRaw", msg => {
return { const handleClick = () => {
label: "View Raw (Left Click) / Copy Raw (Right Click)", if (settings.store.clickMethod === "Right") {
icon: CopyIcon, copyWithToast(msg.content);
message: msg, } else {
channel: ChannelStore.getChannel(msg.channel_id), openViewRawModal(msg);
onClick: () => openViewRawModal(msg), }
onContextMenu: e => { };
const handleContextMenu = e => {
if (settings.store.clickMethod === "Left") {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
copyWithToast(msg.content); copyWithToast(msg.content);
} else {
e.preventDefault();
e.stopPropagation();
openViewRawModal(msg);
} }
}; };
const label = settings.store.clickMethod === "Right"
? "Copy Raw (Left Click) / View Raw (Right Click)"
: "View Raw (Left Click) / Copy Raw (Right Click)";
return {
label,
icon: CopyIcon,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: handleClick,
onContextMenu: handleContextMenu
};
}); });
}, },

View File

@ -44,7 +44,10 @@ const settings = definePluginSettings({
], ],
description: "Choose the greet mode" description: "Choose the greet mode"
} }
}); }).withPrivateSettings<{
multiGreetChoices?: string[];
unholyMultiGreetEnabled?: boolean;
}>();
const MessageActions = findByPropsLazy("sendGreetMessage"); const MessageActions = findByPropsLazy("sendGreetMessage");
@ -73,7 +76,7 @@ function greet(channel: Channel, message: Message, stickers: string[]) {
function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], message: Message, channel: Channel; }) { function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], message: Message, channel: Channel; }) {
const s = settings.use(["greetMode", "multiGreetChoices"] as any) as { greetMode: GreetMode, multiGreetChoices: string[]; }; const s = settings.use(["greetMode", "multiGreetChoices"]);
const { greetMode, multiGreetChoices = [] } = s; const { greetMode, multiGreetChoices = [] } = s;
return ( return (
@ -112,7 +115,7 @@ function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], messag
))} ))}
</Menu.MenuGroup> </Menu.MenuGroup>
{!(settings.store as any).unholyMultiGreetEnabled ? null : ( {!settings.store.unholyMultiGreetEnabled ? null : (
<> <>
<Menu.MenuSeparator /> <Menu.MenuSeparator />

View File

@ -77,14 +77,14 @@ export async function authorizeCloud() {
permissions={0n} permissions={0n}
clientId={clientId} clientId={clientId}
cancelCompletesFlow={false} cancelCompletesFlow={false}
callback={async (callbackUrl: string) => { callback={async ({ location }: any) => {
if (!callbackUrl) { if (!location) {
Settings.cloud.authenticated = false; Settings.cloud.authenticated = false;
return; return;
} }
try { try {
const res = await fetch(callbackUrl, { const res = await fetch(location, {
headers: new Headers({ Accept: "application/json" }) headers: new Headers({ Accept: "application/json" })
}); });
const { secret } = await res.json(); const { secret } = await res.json();

View File

@ -315,6 +315,26 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "UwU", name: "UwU",
id: 691413039156690994n, id: 691413039156690994n,
}, },
amia: {
name: "amia",
id: 142007603549962240n
},
phil: {
name: "phil",
id: 305288513941667851n
},
ImLvna: {
name: "Luna <3",
id: 174200708818665472n
},
rad: {
name: "rad",
id: 113027285765885952n
},
HypedDomi: {
name: "HypedDomi",
id: 354191516979429376n
}
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly

View File

@ -37,7 +37,7 @@ export const importApngJs = makeLazy(async () => {
}); });
// https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk // https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
export enum ApngDisposeOp { export const enum ApngDisposeOp {
/** /**
* no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. * no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
*/ */
@ -53,7 +53,7 @@ export enum ApngDisposeOp {
} }
// TODO: Might need to somehow implement this // TODO: Might need to somehow implement this
export enum ApngBlendOp { export const enum ApngBlendOp {
SOURCE, SOURCE,
OVER OVER
} }

View File

@ -17,9 +17,11 @@
*/ */
import { MessageObject } from "@api/MessageEvents"; import { MessageObject } from "@api/MessageEvents";
import { findByPropsLazy, findLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { ChannelStore, ComponentDispatch, GuildStore, PrivateChannelsStore, SelectedChannelStore } from "@webpack/common"; import { ChannelStore, ComponentDispatch, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, SelectedChannelStore, SelectedGuildStore, UserUtils } from "@webpack/common";
import { Guild, Message } from "discord-types/general"; import { Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
const PreloadedUserSettings = findLazy(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings")); const PreloadedUserSettings = findLazy(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings"));
const MessageActions = findByPropsLazy("editMessage", "sendMessage"); const MessageActions = findByPropsLazy("editMessage", "sendMessage");
@ -77,3 +79,48 @@ export function sendMessage(
return MessageActions.sendMessage(channelId, messageData, waitForChannelReady, extra); return MessageActions.sendMessage(channelId, messageData, waitForChannelReady, extra);
} }
export function openImageModal(url: string, props?: Partial<React.ComponentProps<ImageModal>>): string {
return openModal(modalProps => (
<ModalRoot
{...modalProps}
className={ModalImageClasses.modal}
size={ModalSize.DYNAMIC}>
<ImageModal
className={ModalImageClasses.image}
original={url}
placeholder={url}
src={url}
renderLinkComponent={props => <MaskedLink {...props} />}
shouldHideMediaOptions={false}
shouldAnimate
{...props}
/>
</ModalRoot>
));
}
const openProfile = findByCodeLazy("friendToken", "USER_PROFILE_MODAL_OPEN");
export async function openUserProfile(id: string) {
const user = await UserUtils.fetchUser(id);
if (!user) throw new Error("No such user: " + id);
const guildId = SelectedGuildStore.getGuildId();
openProfile({
userId: id,
guildId,
channelId: SelectedChannelStore.getChannelId(),
analyticsLocation: {
page: guildId ? "Guild Channel" : "DM Channel",
section: "Profile Popout"
}
});
}
/**
* Get the unique username for a user. Returns user.username for pomelo people, user.tag otherwise
*/
export function getUniqueUsername(user: User) {
return user.discriminator === "0" ? user.username : user.tag;
}

View File

@ -31,4 +31,5 @@ export * from "./onceDefined";
export * from "./onlyOnce"; export * from "./onlyOnce";
export * from "./patches"; export * from "./patches";
export * from "./Queue"; export * from "./Queue";
export * from "./react";
export * from "./text"; export * from "./text";

View File

@ -16,19 +16,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { filters, mapMangledModuleLazy } from "@webpack"; import { filters, findByCode, mapMangledModuleLazy } from "@webpack";
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react"; import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
import { LazyComponent } from "./react"; import { LazyComponent } from "./react";
export enum ModalSize { export const enum ModalSize {
SMALL = "small", SMALL = "small",
MEDIUM = "medium", MEDIUM = "medium",
LARGE = "large", LARGE = "large",
DYNAMIC = "dynamic", DYNAMIC = "dynamic",
} }
enum ModalTransitionState { const enum ModalTransitionState {
ENTERING, ENTERING,
ENTERED, ENTERED,
EXITING, EXITING,
@ -107,6 +107,25 @@ export const Modals = mapMangledModuleLazy(".closeWithCircleBackground", {
}>; }>;
}; };
export type ImageModal = ComponentType<{
className?: string;
src: string;
placeholder: string;
original: string;
width?: number;
height?: number;
animated?: boolean;
responsive?: boolean;
renderLinkComponent(props: any): ReactNode;
maxWidth?: number;
maxHeight?: number;
shouldAnimate?: boolean;
onClose?(): void;
shouldHideMediaOptions?: boolean;
}>;
export const ImageModal = LazyComponent(() => findByCode(".renderLinkComponent", ".responsive") as ImageModal);
export const ModalRoot = LazyComponent(() => Modals.ModalRoot); export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
export const ModalHeader = LazyComponent(() => Modals.ModalHeader); export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
export const ModalContent = LazyComponent(() => Modals.ModalContent); export const ModalContent = LazyComponent(() => Modals.ModalContent);

View File

@ -149,12 +149,6 @@ export async function putCloudSettings() {
VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4)); VencordNative.settings.set(JSON.stringify(PlainSettings, null, 4));
cloudSettingsLogger.info("Settings uploaded to cloud successfully"); cloudSettingsLogger.info("Settings uploaded to cloud successfully");
showNotification({
title: "Cloud Settings",
body: "Synchronized your settings to the cloud!",
color: "var(--green-360)",
noPersist: true
});
} catch (e: any) { } catch (e: any) {
cloudSettingsLogger.error("Failed to sync up", e); cloudSettingsLogger.error("Failed to sync up", e);
showNotification({ showNotification({

View File

@ -117,7 +117,7 @@ export interface PluginDef {
tags?: string[]; tags?: string[];
} }
export enum OptionType { export const enum OptionType {
STRING, STRING,
NUMBER, NUMBER,
BIGINT, BIGINT,
@ -260,25 +260,29 @@ type SettingsStore<D extends SettingsDefinition> = {
}; };
/** An instance of defined plugin settings */ /** An instance of defined plugin settings */
export interface DefinedSettings<D extends SettingsDefinition = SettingsDefinition, C extends SettingsChecks<D> = {}> { export interface DefinedSettings<
Def extends SettingsDefinition = SettingsDefinition,
Checks extends SettingsChecks<Def> = {},
PrivateSettings extends object = {}
> {
/** Shorthand for `Vencord.Settings.plugins.PluginName`, but with typings */ /** Shorthand for `Vencord.Settings.plugins.PluginName`, but with typings */
store: SettingsStore<D>; store: SettingsStore<Def> & PrivateSettings;
/** /**
* React hook for getting the settings for this plugin * React hook for getting the settings for this plugin
* @param filter optional filter to avoid rerenders for irrelavent settings * @param filter optional filter to avoid rerenders for irrelavent settings
*/ */
use<F extends Extract<keyof D, string>>(filter?: F[]): Pick<SettingsStore<D>, F>; use<F extends Extract<keyof Def | keyof PrivateSettings, string>>(filter?: F[]): Pick<SettingsStore<Def> & PrivateSettings, F>;
/** Definitions of each setting */ /** Definitions of each setting */
def: D; def: Def;
/** Setting methods with return values that could rely on other settings */ /** Setting methods with return values that could rely on other settings */
checks: C; checks: Checks;
/** /**
* Name of the plugin these settings belong to, * Name of the plugin these settings belong to,
* will be an empty string until plugin is initialized * will be an empty string until plugin is initialized
*/ */
pluginName: string; pluginName: string;
withPrivateSettings<T>(): this & { store: T; }; withPrivateSettings<T extends object>(): DefinedSettings<Def, Checks, T>;
} }
export type PartialExcept<T, R extends keyof T> = Partial<T> & Required<Pick<T, R>>; export type PartialExcept<T, R extends keyof T> = Partial<T> & Required<Pick<T, R>>;

View File

@ -0,0 +1,24 @@
/*
* 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 { findByPropsLazy } from "@webpack";
import * as t from "./types/classes";
export const ModalImageClasses: t.ImageModalClasses = findByPropsLazy("image", "modal");
export const ButtonWrapperClasses: t.ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent");

View File

@ -17,7 +17,7 @@
*/ */
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { filters, findByPropsLazy, waitFor } from "@webpack"; import { filters, waitFor } from "@webpack";
import { waitForComponent } from "./internal"; import { waitForComponent } from "./internal";
import * as t from "./types/components"; import * as t from "./types/components";
@ -44,16 +44,18 @@ export let Popout: t.Popout;
export let Dialog: t.Dialog; export let Dialog: t.Dialog;
export let TabBar: any; export let TabBar: any;
export let Paginator: t.Paginator; export let Paginator: t.Paginator;
export let ScrollerThin: t.ScrollerThin;
export let Clickable: t.Clickable;
export let Avatar: t.Avatar;
// token lagger real // token lagger real
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */ /** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
export let useToken: t.useToken; export let useToken: t.useToken;
export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", m => m?.type?.toString().includes("MASKED_LINK)"));
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]); export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>;
waitFor("FormItem", m => { waitFor("FormItem", m => {
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator } = m); ({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar } = m);
Forms = m; Forms = m;
}); });

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export * from "./classes";
export * from "./components"; export * from "./components";
export * from "./menu"; export * from "./menu";
export * from "./react"; export * from "./react";
@ -24,4 +25,3 @@ export * as ComponentTypes from "./types/components.d";
export * as MenuTypes from "./types/menu.d"; export * as MenuTypes from "./types/menu.d";
export * as UtilTypes from "./types/utils.d"; export * as UtilTypes from "./types/utils.d";
export * from "./utils"; export * from "./utils";

40
src/webpack/common/types/classes.d.ts vendored Normal file
View File

@ -0,0 +1,40 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface ImageModalClasses {
image: string,
modal: string,
responsiveWidthMobile: string;
}
export interface ButtonWrapperClasses {
hoverScale: string;
buttonWrapper: string;
button: string;
iconMask: string;
buttonContent: string;
icon: string;
pulseIcon: string;
pulseButton: string;
notificationDot: string;
sparkleContainer: string;
sparkleStar: string;
sparklePlus: string;
sparkle: string;
active: string;
}

View File

@ -397,3 +397,53 @@ export type Paginator = ComponentType<{
onPageChange?(page: number): void; onPageChange?(page: number): void;
hideMaxPage?: boolean; hideMaxPage?: boolean;
}>; }>;
export type MaskedLink = ComponentType<{
onClick(): void;
trusted: boolean;
title: string,
href: string;
}>;
export type ScrollerThin = ComponentType<PropsWithChildren<{
className?: string;
style?: CSSProperties;
dir?: "ltr";
orientation?: "horizontal" | "vertical";
paddingFix?: boolean;
fade?: boolean;
onClose?(): void;
onScroll?(): void;
}>>;
export type Clickable = ComponentType<PropsWithChildren<{
className?: string;
href?: string;
ignoreKeyPress?: boolean;
onClick?(): void;
onKeyPress?(): void;
}>>;
export type Avatar = ComponentType<PropsWithChildren<{
className?: string;
src?: string;
size?: "SIZE_16" | "SIZE_20" | "SIZE_24" | "SIZE_32" | "SIZE_40" | "SIZE_48" | "SIZE_56" | "SIZE_80" | "SIZE_120";
statusColor?: string;
statusTooltip?: string;
statusBackdropColor?: string;
isMobile?: boolean;
isTyping?: boolean;
isSpeaking?: boolean;
typingIndicatorRef?: unknown;
"aria-hidden"?: boolean;
"aria-label"?: string;
}>>;

View File

@ -44,6 +44,7 @@ export interface Menu {
onChildrenScroll?: Function; onChildrenScroll?: Function;
childRowHeight?: number; childRowHeight?: number;
listClassName?: string; listClassName?: string;
disabled?: boolean;
}>; }>;
MenuCheckboxItem: RC<{ MenuCheckboxItem: RC<{
id: string; id: string;

View File

@ -20,14 +20,23 @@ import { Channel } from "discord-types/general";
import { FluxDispatcher, FluxEvents } from "./utils"; import { FluxDispatcher, FluxEvents } from "./utils";
type GenericFunction = (...args: any[]) => any;
export class FluxStore { export class FluxStore {
constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial<Record<FluxEvents, (data: any) => void>>); constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial<Record<FluxEvents, (data: any) => void>>);
addChangeListener(callback: () => void): void;
addReactChangeListener(callback: () => void): void;
removeChangeListener(callback: () => void): void;
removeReactChangeListener(callback: () => void): void;
emitChange(): void; emitChange(): void;
getDispatchToken(): string; getDispatchToken(): string;
getName(): string; getName(): string;
initialize(): void; initialize(): void;
initializeIfNeeded(): void; initializeIfNeeded(): void;
registerActionHandlers: GenericFunction;
syncWith: GenericFunction;
waitFor: GenericFunction;
__getLocalVars(): Record<string, any>; __getLocalVars(): Record<string, any>;
} }