From 30bc979c8ddcb0da04fd494fdf2b8631183e3731 Mon Sep 17 00:00:00 2001 From: jd <28711824+jewdev@users.noreply.github.com> Date: Sun, 31 Dec 2023 02:06:19 +0200 Subject: [PATCH 001/356] feat(Urban Dictionary): Chooses top rated definition & more results. (#2080) --- src/plugins/urbanDictionary/README.md | 13 +++++++++++++ src/plugins/urbanDictionary/index.ts | 22 ++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/plugins/urbanDictionary/README.md diff --git a/src/plugins/urbanDictionary/README.md b/src/plugins/urbanDictionary/README.md new file mode 100644 index 000000000..e065456a3 --- /dev/null +++ b/src/plugins/urbanDictionary/README.md @@ -0,0 +1,13 @@ +# Urban Dictionary + +Use /urban slash command to search for a definition for a word on [Urban Dictionary](https://www.urbandictionary.com/). + +## Preview + +![preview](https://i.imgur.com/1zwzj38.png) + +## Usage + +- Enable this plugin +- Set plugin settings as desired +- Type /urban and start getting definitions right into your Discord client. diff --git a/src/plugins/urbanDictionary/index.ts b/src/plugins/urbanDictionary/index.ts index 840fe5cb6..89dcdcba4 100644 --- a/src/plugins/urbanDictionary/index.ts +++ b/src/plugins/urbanDictionary/index.ts @@ -18,14 +18,24 @@ import { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands"; import { ApplicationCommandInputType } from "@api/Commands/types"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; + +const settings = definePluginSettings({ + resultsAmount: { + type: OptionType.NUMBER, + description: "The amount of results you want to get (more gives better results, but is slower)", + default: 10 + } +}); export default definePlugin({ name: "UrbanDictionary", description: "Search for a word on Urban Dictionary via /urban slash command", authors: [Devs.jewdev], dependencies: ["CommandsAPI"], + settings, commands: [ { name: "urban", @@ -41,12 +51,16 @@ export default definePlugin({ ], execute: async (args, ctx) => { try { - const query = encodeURIComponent(args[0].value); - const { list: [definition] } = await (await fetch(`https://api.urbandictionary.com/v0/define?term=${query}`)).json(); + const query: string = encodeURIComponent(args[0].value); + const { list } = await fetch(`https://api.urbandictionary.com/v0/define?term=${query}&per_page=${settings.store.resultsAmount}`).then(response => response.json()); - if (!definition) + if (!list.length) return void sendBotMessage(ctx.channel.id, { content: "No results found." }); + const definition = list.reduce((prev, curr) => { + return prev.thumbs_up > curr.thumbs_up ? prev : curr; + }); + const linkify = (text: string) => text .replaceAll("\r\n", "\n") .replace(/([*>_`~\\])/gsi, "\\$1") From 7e395fc6968aced5d45fae55db646def1d555c49 Mon Sep 17 00:00:00 2001 From: V Date: Sun, 31 Dec 2023 05:02:05 +0100 Subject: [PATCH 002/356] Update codeburger mirror --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8611babd7..a43c9f834 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Vencord -[![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) +[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/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/Vee/cord) The cutest Discord client mod From d0dfdbbd5fc263ab105fb9d9f1c2f1295a4c8c7e Mon Sep 17 00:00:00 2001 From: Lewis Crichton Date: Thu, 28 Dec 2023 02:02:49 +0000 Subject: [PATCH 003/356] fix: Vencord_cloudSecret check (#2077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit finally got around to fixing it - `null` is never a valid return value, it's `undefined` 🤦 --- src/Vencord.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Vencord.ts b/src/Vencord.ts index a106a0b7d..29e965fa0 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -44,7 +44,7 @@ async function syncSettings() { // pre-check for local shared settings if ( Settings.cloud.authenticated && - await dsGet("Vencord_cloudSecret") === null // this has been enabled due to local settings share or some other bug + !await dsGet("Vencord_cloudSecret") // this has been enabled due to local settings share or some other bug ) { // show a notification letting them know and tell them how to fix it showNotification({ @@ -145,4 +145,3 @@ document.addEventListener("DOMContentLoaded", () => { })); } }, { once: true }); - From a963a19bdc67400a024253fd8285bf7677be7db1 Mon Sep 17 00:00:00 2001 From: Jack <30497388+FieryFlames@users.noreply.github.com> Date: Wed, 3 Jan 2024 07:37:56 -0500 Subject: [PATCH 004/356] fix(Decor): Fix AvatarDecorationModalPreview find (#2089) --- src/plugins/decor/ui/components/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/decor/ui/components/index.ts b/src/plugins/decor/ui/components/index.ts index 8f39a10ee..8fe41fc92 100644 --- a/src/plugins/decor/ui/components/index.ts +++ b/src/plugins/decor/ui/components/index.ts @@ -19,7 +19,7 @@ export let DecorationGridItem: DecorationGridItemComponent; export const setDecorationGridItem = v => DecorationGridItem = v; export const AvatarDecorationModalPreview = LazyComponentWebpack(() => { - const component = findComponentByCode("AvatarDecorationModalPreview"); + const component = findComponentByCode(".shopPreviewBanner"); return React.memo(component); }); From 1eb2510353cf3ffac9eb746aba8e08d01a54ac06 Mon Sep 17 00:00:00 2001 From: nexpid <60316309+nexpid@users.noreply.github.com> Date: Wed, 3 Jan 2024 13:49:03 +0100 Subject: [PATCH 005/356] feat(Decor): Enforce guidelines more (#2035) --- src/plugins/decor/index.tsx | 31 +---- src/plugins/decor/settings.tsx | 47 +++++++ .../decor/ui/modals/ChangeDecorationModal.tsx | 110 ++++++++-------- .../decor/ui/modals/CreateDecorationModal.tsx | 121 +++++++++--------- .../decor/ui/modals/GuidelinesModal.tsx | 68 ++++++++++ src/plugins/decor/ui/styles.css | 4 +- 6 files changed, 242 insertions(+), 139 deletions(-) create mode 100644 src/plugins/decor/settings.tsx create mode 100644 src/plugins/decor/ui/modals/GuidelinesModal.tsx diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 4dd7aa0c9..ce546d309 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -6,21 +6,17 @@ import "./ui/styles.css"; -import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import { classes } from "@utils/misc"; -import { closeAllModals } from "@utils/modal"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { FluxDispatcher, Forms, UserStore } from "@webpack/common"; +import { UserStore } from "@webpack/common"; import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants"; import { useAuthorizationStore } from "./lib/stores/AuthorizationStore"; import { useCurrentUserDecorationsStore } from "./lib/stores/CurrentUserDecorationsStore"; import { useUserDecorAvatarDecoration, useUsersDecorationsStore } from "./lib/stores/UsersDecorationsStore"; +import { settings } from "./settings"; import { setDecorationGridDecoration, setDecorationGridItem } from "./ui/components"; import DecorSection from "./ui/components/DecorSection"; @@ -30,27 +26,6 @@ export interface AvatarDecoration { skuId: string; } -const settings = definePluginSettings({ - changeDecoration: { - type: OptionType.COMPONENT, - description: "Change your avatar decoration", - component() { - return
- - - You can also access Decor decorations from the { - e.preventDefault(); - closeAllModals(); - FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" }); - }} - >Profiles page. - -
; - } - } -}); export default definePlugin({ name: "Decor", description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.", diff --git a/src/plugins/decor/settings.tsx b/src/plugins/decor/settings.tsx new file mode 100644 index 000000000..0d3628cc6 --- /dev/null +++ b/src/plugins/decor/settings.tsx @@ -0,0 +1,47 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Link } from "@components/Link"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { closeAllModals } from "@utils/modal"; +import { OptionType } from "@utils/types"; +import { FluxDispatcher, Forms } from "@webpack/common"; + +import DecorSection from "./ui/components/DecorSection"; + +export const settings = definePluginSettings({ + changeDecoration: { + type: OptionType.COMPONENT, + description: "Change your avatar decoration", + component() { + if (!Vencord.Plugins.plugins.Decor.started) return + Enable Decor and restart your client to change your avatar decoration. + ; + + return
+ + + You can also access Decor decorations from the { + e.preventDefault(); + closeAllModals(); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" }); + }} + >Profiles page. + +
; + } + }, + agreedToGuidelines: { + type: OptionType.BOOLEAN, + description: "Agreed to guidelines", + hidden: true, + default: false + } +}); diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx index bed007174..f2a482818 100644 --- a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx @@ -4,11 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; -import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common"; import { User } from "discord-types/general"; @@ -18,6 +19,7 @@ import { GUILD_ID, INVITE_KEY } from "../../lib/constants"; import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; import { decorationToAvatarDecoration } from "../../lib/utils/decoration"; +import { settings } from "../../settings"; import { cl, requireAvatarDecorationModal } from "../"; import { AvatarDecorationModalPreview } from "../components"; import DecorationGridCreate from "../components/DecorationGridCreate"; @@ -25,6 +27,7 @@ import DecorationGridNone from "../components/DecorationGridNone"; import DecorDecorationGridDecoration from "../components/DecorDecorationGridDecoration"; import SectionedGridList from "../components/SectionedGridList"; import { openCreateDecorationModal } from "./CreateDecorationModal"; +import { openGuidelinesModal } from "./GuidelinesModal"; const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); @@ -83,7 +86,7 @@ function SectionHeader({ section }: { section: Section; }) { ; } -export default function ChangeDecorationModal(props: any) { +function ChangeDecorationModal(props: ModalProps) { // undefined = not trying, null = none, Decoration = selected const [tryingDecoration, setTryingDecoration] = useState(undefined); const isTryingDecoration = typeof tryingDecoration !== "undefined"; @@ -116,6 +119,7 @@ export default function ChangeDecorationModal(props: any) { const data = [ { title: "Your Decorations", + subtitle: "You can delete your own decorations by right clicking on them.", sectionKey: "ownDecorations", items: ["none", ...ownDecorations, "create"] }, @@ -148,60 +152,62 @@ export default function ChangeDecorationModal(props: any) { className={cl("change-decoration-modal-content")} scrollbarType="none" > - { - if (typeof item === "string") { - switch (item) { - case "none": - return setTryingDecoration(null)} - />; - case "create": - return - {tooltipProps => + { + if (typeof item === "string") { + switch (item) { + case "none": + return setTryingDecoration(null)} + />; + case "create": + return + {tooltipProps => { }} + />} + ; + } + } else { + return + {tooltipProps => ( + { }} - />} - ; + className={cl("change-decoration-modal-decoration")} + onSelect={item.reviewed !== false ? () => setTryingDecoration(item) : () => { }} + isSelected={activeSelectedDecoration?.hash === item.hash} + decoration={item} + /> + )} + ; } - } else { - return - {tooltipProps => ( - setTryingDecoration(item) : () => { }} - isSelected={activeSelectedDecoration?.hash === item.hash} - decoration={item} - /> - )} - ; - } - }} - getItemKey={item => typeof item === "string" ? item : item.hash} - getSectionKey={section => section.sectionKey} - renderSectionHeader={section => } - sections={data} - /> -
- typeof item === "string" ? item : item.hash} + getSectionKey={section => section.sectionKey} + renderSectionHeader={section => } + sections={data} /> - {isActiveDecorationPreset && Part of the {activeDecorationPreset.name} Preset} - {typeof activeSelectedDecoration === "object" && - - {activeSelectedDecoration?.alt} - - } - {activeDecorationHasAuthor && Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}} -
+
+ + {isActiveDecorationPreset && Part of the {activeDecorationPreset.name} Preset} + {typeof activeSelectedDecoration === "object" && + + {activeSelectedDecoration?.alt} + + } + {activeDecorationHasAuthor && Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}} +
+
diff --git a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx index a5937b0dd..eea79d86e 100644 --- a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx @@ -4,10 +4,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import ErrorBoundary from "@components/ErrorBoundary"; import { Link } from "@components/Link"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; -import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common"; @@ -21,6 +22,8 @@ const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); const FileUpload = findComponentByCodeLazy("fileUploadInput,"); +const { default: HelpMessage, HelpMessageTypes } = findByPropsLazy("HelpMessageTypes"); + function useObjectURL(object: Blob | MediaSource | null) { const [url, setUrl] = useState(null); @@ -39,7 +42,7 @@ function useObjectURL(object: Blob | MediaSource | null) { return url; } -export default function CreateDecorationModal(props) { +function CreateDecorationModal(props: ModalProps) { const [name, setName] = useState(""); const [file, setFile] = useState(null); const [submitting, setSubmitting] = useState(false); @@ -75,65 +78,69 @@ export default function CreateDecorationModal(props) { className={cl("create-decoration-modal-content")} scrollbarType="none" > -
-
- {error !== null && {error.message}} - - + + Make sure your decoration does not violate + the guidelines + before submitting it. + +
+
+ {error !== null && {error.message}} + + + + File should be APNG or PNG. + + + + + + This name will be used when referring to this decoration. + + +
+
+ - - File should be APNG or PNG. - - - - - - This name will be used when referring to this decoration. - - +
-
- -
-
- - Make sure your decoration does not violate - the guidelines - before creating your decoration. -
You can receive updates on your decoration's review by joining { - e.preventDefault(); - if (!GuildStore.getGuild(GUILD_ID)) { - const inviteAccepted = await openInviteModal(INVITE_KEY); - if (inviteAccepted) { + +
You can receive updates on your decoration's review by joining { + e.preventDefault(); + if (!GuildStore.getGuild(GUILD_ID)) { + const inviteAccepted = await openInviteModal(INVITE_KEY); + if (inviteAccepted) { + closeAllModals(); + FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + } + } else { closeAllModals(); FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + NavigationRouter.transitionToGuild(GUILD_ID); } - } else { - closeAllModals(); - FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); - NavigationRouter.transitionToGuild(GUILD_ID); - } - }} - > - Decor's Discord server - . -
+ }} + > + Decor's Discord server + . +
+ + + + ; +} + +export const openGuidelinesModal = () => + requireAvatarDecorationModal().then(() => openModal(props => )); diff --git a/src/plugins/decor/ui/styles.css b/src/plugins/decor/ui/styles.css index ff10c82fa..9730efb7e 100644 --- a/src/plugins/decor/ui/styles.css +++ b/src/plugins/decor/ui/styles.css @@ -8,7 +8,7 @@ display: flex; border-radius: 5px 5px 0 0; padding: 0 16px; - gap: 4px + gap: 4px; } .vc-decor-change-decoration-modal-preview { @@ -72,7 +72,7 @@ .vc-decor-sectioned-grid-list-grid { display: flex; flex-wrap: wrap; - gap: 8px + gap: 8px; } .vc-decor-section-remove-margin { From 4a1c85c8adc04e4874b73b32924d24bd0839aab9 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 3 Jan 2024 09:58:09 -0300 Subject: [PATCH 006/356] Decor: only search for DecorationModalStyles once --- src/plugins/decor/ui/index.ts | 3 ++- src/plugins/decor/ui/modals/ChangeDecorationModal.tsx | 5 ++--- src/plugins/decor/ui/modals/CreateDecorationModal.tsx | 5 +---- src/plugins/decor/ui/modals/GuidelinesModal.tsx | 5 +---- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/plugins/decor/ui/index.ts b/src/plugins/decor/ui/index.ts index 52b169d77..0ead602e2 100644 --- a/src/plugins/decor/ui/index.ts +++ b/src/plugins/decor/ui/index.ts @@ -5,9 +5,10 @@ */ import { classNameFactory } from "@api/Styles"; -import { extractAndLoadChunksLazy } from "@webpack"; +import { extractAndLoadChunksLazy, findByPropsLazy } from "@webpack"; export const cl = classNameFactory("vc-decor-"); +export const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); export const requireAvatarDecorationModal = extractAndLoadChunksLazy(["openAvatarDecorationModal:"]); export const requireCreateStickerModal = extractAndLoadChunksLazy(["stickerInspected]:"]); diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx index f2a482818..5fbe165ce 100644 --- a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx @@ -10,7 +10,7 @@ import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { findComponentByCodeLazy } from "@webpack"; import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common"; import { User } from "discord-types/general"; @@ -20,7 +20,7 @@ import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; import { decorationToAvatarDecoration } from "../../lib/utils/decoration"; import { settings } from "../../settings"; -import { cl, requireAvatarDecorationModal } from "../"; +import { cl, DecorationModalStyles, requireAvatarDecorationModal } from "../"; import { AvatarDecorationModalPreview } from "../components"; import DecorationGridCreate from "../components/DecorationGridCreate"; import DecorationGridNone from "../components/DecorationGridNone"; @@ -30,7 +30,6 @@ import { openCreateDecorationModal } from "./CreateDecorationModal"; import { openGuidelinesModal } from "./GuidelinesModal"; const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); -const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); function usePresets() { const [presets, setPresets] = useState([]); diff --git a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx index eea79d86e..0dcf855ef 100644 --- a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx @@ -14,12 +14,9 @@ import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, Text import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; -import { cl, requireAvatarDecorationModal, requireCreateStickerModal } from "../"; +import { cl, DecorationModalStyles, requireAvatarDecorationModal, requireCreateStickerModal } from "../"; import { AvatarDecorationModalPreview } from "../components"; - -const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); - const FileUpload = findComponentByCodeLazy("fileUploadInput,"); const { default: HelpMessage, HelpMessageTypes } = findByPropsLazy("HelpMessageTypes"); diff --git a/src/plugins/decor/ui/modals/GuidelinesModal.tsx b/src/plugins/decor/ui/modals/GuidelinesModal.tsx index ab1bf441e..716aea6bd 100644 --- a/src/plugins/decor/ui/modals/GuidelinesModal.tsx +++ b/src/plugins/decor/ui/modals/GuidelinesModal.tsx @@ -6,15 +6,12 @@ import { Link } from "@components/Link"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { findByPropsLazy } from "@webpack"; import { Button, Forms, Text } from "@webpack/common"; import { settings } from "../../settings"; -import { cl, requireAvatarDecorationModal } from "../"; +import { cl, DecorationModalStyles, requireAvatarDecorationModal } from "../"; import { openCreateDecorationModal } from "./CreateDecorationModal"; -const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); - function GuidelinesModal(props: ModalProps) { return Date: Fri, 5 Jan 2024 22:07:00 +0100 Subject: [PATCH 007/356] fix cobu sync --- .github/workflows/codeberg-mirror.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeberg-mirror.yml b/.github/workflows/codeberg-mirror.yml index 647a43135..1b2266ee7 100644 --- a/.github/workflows/codeberg-mirror.yml +++ b/.github/workflows/codeberg-mirror.yml @@ -18,5 +18,5 @@ jobs: fetch-depth: 0 - uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1 with: - target_repo_url: "git@codeberg.org:Ven/cord.git" + target_repo_url: "git@codeberg.org:Vee/cord.git" ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }} From 88fc15752d95881f2ba90d935c7dc5404649c806 Mon Sep 17 00:00:00 2001 From: sunnie <78964224+sunnniee@users.noreply.github.com> Date: Fri, 5 Jan 2024 23:12:13 +0200 Subject: [PATCH 008/356] webContextMenus: add Show My Camera item (#2086) --- src/plugins/webContextMenus.web/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index eb076dfd0..50a1b90a7 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -169,6 +169,15 @@ export default definePlugin({ match: /let\{text:\i=""/, replace: "return [null,null];$&" } + }, + + // Add back "Show My Camera" context menu + { + find: '.default("MediaEngineWebRTC");', + replacement: { + match: /supports\(\i\)\{switch\(\i\)\{case (\i).Features/, + replace: "$&.DISABLE_VIDEO:return true;case $1.Features" + } } ], From 3eada99ad623ae783a39eb2c8238ac66ed371132 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 5 Jan 2024 22:15:54 +0100 Subject: [PATCH 009/356] fix(ToolBox): don't add entries of disabled plugins --- src/plugins/vencordToolbox/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index 2b0ce14e6..0a805a0d2 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -33,7 +33,7 @@ function VencordPopout(onClose: () => void) { const pluginEntries = [] as ReactNode[]; for (const plugin of Object.values(Vencord.Plugins.plugins)) { - if (plugin.toolboxActions) { + if (plugin.toolboxActions && Vencord.Plugins.isPluginEnabled(plugin.name)) { pluginEntries.push( Date: Fri, 5 Jan 2024 23:31:09 +0100 Subject: [PATCH 010/356] fix wrongly patching 'Events' context menu --- src/plugins/biggerStreamPreview/index.tsx | 2 +- src/plugins/copyUserURLs/index.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/biggerStreamPreview/index.tsx b/src/plugins/biggerStreamPreview/index.tsx index acad564da..40bbe30a8 100644 --- a/src/plugins/biggerStreamPreview/index.tsx +++ b/src/plugins/biggerStreamPreview/index.tsx @@ -82,7 +82,7 @@ export const streamContextPatch: NavContextMenuPatchCallback = (children, { stre }; export const userContextPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => { - return addViewStreamContext(children, { userId: user.id }); + if (user) return addViewStreamContext(children, { userId: user.id }); }; export default definePlugin({ diff --git a/src/plugins/copyUserURLs/index.tsx b/src/plugins/copyUserURLs/index.tsx index e3c336fb2..9f69674cf 100644 --- a/src/plugins/copyUserURLs/index.tsx +++ b/src/plugins/copyUserURLs/index.tsx @@ -30,6 +30,8 @@ interface UserContextProps { } const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => { + if (!user) return; + children.push( Date: Fri, 5 Jan 2024 23:51:49 +0100 Subject: [PATCH 011/356] ReverseImageSearch: add support for image modal --- src/plugins/reverseImageSearch/index.tsx | 116 ++++++++++++----------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/src/plugins/reverseImageSearch/index.tsx b/src/plugins/reverseImageSearch/index.tsx index 811e7ff08..6c5f3e729 100644 --- a/src/plugins/reverseImageSearch/index.tsx +++ b/src/plugins/reverseImageSearch/index.tsx @@ -36,62 +36,68 @@ function search(src: string, engine: string) { open(engine + encodeURIComponent(src), "_blank"); } -const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (!props) return; - const { reverseImageSearchType, itemHref, itemSrc } = props; +function makeSearchItem(src: string) { + return ( + + {Object.keys(Engines).map((engine, i) => { + const key = "search-image-" + engine; + return ( + + = 3 // Do not round Google, Yandex & SauceNAO + ? "50%" + : void 0 + }} + aria-hidden="true" + height={16} + width={16} + src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")} + /> + {engine} + + } + action={() => search(src, Engines[engine])} + /> + ); + })} + + + All + + } + action={() => Object.values(Engines).forEach(e => search(src, e))} + /> + + ); +} - if (!reverseImageSearchType || reverseImageSearchType !== "img") return; +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (props?.reverseImageSearchType !== "img") return; - const src = itemHref ?? itemSrc; + const src = props.itemHref ?? props.itemSrc; const group = findGroupChildrenByChildId("copy-link", children); - if (group) { - group.push(( - - {Object.keys(Engines).map((engine, i) => { - const key = "search-image-" + engine; - return ( - - = 3 // Do not round Google, Yandex & SauceNAO - ? "50%" - : void 0 - }} - aria-hidden="true" - height={16} - width={16} - src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")} - /> - {engine} - - } - action={() => search(src, Engines[engine])} - /> - ); - })} - - - All - - } - action={() => Object.values(Engines).forEach(e => search(src, e))} - /> - - )); - } + group?.push(makeSearchItem(src)); +}; + +const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (!props?.src) return; + + const group = findGroupChildrenByChildId("copy-native-link", children) ?? children; + group.push(makeSearchItem(props.src)); }; export default definePlugin({ @@ -111,10 +117,12 @@ export default definePlugin({ ], start() { - addContextMenuPatch("message", imageContextMenuPatch); + addContextMenuPatch("message", messageContextMenuPatch); + addContextMenuPatch("image-context", imageContextMenuPatch); }, stop() { - removeContextMenuPatch("message", imageContextMenuPatch); + removeContextMenuPatch("message", messageContextMenuPatch); + removeContextMenuPatch("image-context", imageContextMenuPatch); } }); From 6530526fb2eff5881fe677c6437ba73cc31597b0 Mon Sep 17 00:00:00 2001 From: Moxie <124418090+moxie-coder@users.noreply.github.com> Date: Fri, 5 Jan 2024 18:10:16 -0500 Subject: [PATCH 012/356] Updater: Fix grammar (1 Updates => 1 Update) (#2084) Co-authored-by: V --- src/components/VencordSettings/UpdaterTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx index 81433960f..a221bb38b 100644 --- a/src/components/VencordSettings/UpdaterTab.tsx +++ b/src/components/VencordSettings/UpdaterTab.tsx @@ -113,7 +113,7 @@ function Updatable(props: CommonProps) { ) : ( - {isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"} + {isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"} )} From be9ec3b7ac79334e5c753d6377894496006f5fdf Mon Sep 17 00:00:00 2001 From: Grzesiek11 Date: Sat, 6 Jan 2024 00:23:09 +0100 Subject: [PATCH 013/356] SendTimestamps: add setting to disable message replacement (#2076) --- src/plugins/sendTimestamps/index.tsx | 19 ++++++++++++++++--- src/utils/constants.ts | 4 ++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx index 7904545c8..6d488add6 100644 --- a/src/plugins/sendTimestamps/index.tsx +++ b/src/plugins/sendTimestamps/index.tsx @@ -19,14 +19,23 @@ import "./styles.css"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; +import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Devs } from "@utils/constants"; import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { Button, ButtonLooks, ButtonWrapperClasses, Forms, Parser, Select, Tooltip, useMemo, useState } from "@webpack/common"; +const settings = definePluginSettings({ + replaceMessageContents: { + description: "Replace timestamps in message contents", + type: OptionType.BOOLEAN, + default: true, + }, +}); + function parseTime(time: string) { const cleanTime = time.slice(1, -1).replace(/(\d)(AM|PM)$/i, "$1 $2"); @@ -116,9 +125,11 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi export default definePlugin({ name: "SendTimestamps", description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!", - authors: [Devs.Ven, Devs.Tyler], + authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11], dependencies: ["MessageEventsAPI"], + settings: settings, + patches: [ { find: "ChannelTextAreaButtons", @@ -131,7 +142,9 @@ export default definePlugin({ start() { this.listener = addPreSendListener((_, msg) => { - msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); + if (settings.store.replaceMessageContents) { + msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); + } }); }, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 2962df06f..a94ba0fc3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -399,6 +399,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "maisy", id: 257109471589957632n, }, + Grzesiek11: { + name: "Grzesiek11", + id: 368475654662127616n, + }, } satisfies Record); // iife so #__PURE__ works correctly From d5c58ab2b3fa24b00ab310fa7152174f2d235267 Mon Sep 17 00:00:00 2001 From: Hari Rana Date: Fri, 5 Jan 2024 18:55:14 -0500 Subject: [PATCH 014/356] updater: Add top and bottom margins between commits (#2042) Co-authored-by: V --- src/components/VencordSettings/UpdaterTab.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx index a221bb38b..0a5d1f149 100644 --- a/src/components/VencordSettings/UpdaterTab.tsx +++ b/src/components/VencordSettings/UpdaterTab.tsx @@ -81,9 +81,12 @@ function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) { return ( - + {updates.map(({ hash, author, message }) => ( -
+
Date: Sat, 6 Jan 2024 01:02:09 +0100 Subject: [PATCH 015/356] TextReplace: allow replacing with nothing (#1933) --- src/plugins/textReplace/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index 5d66d2265..416ce83fb 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -213,7 +213,7 @@ function applyRules(content: string): string { if (stringRules) { for (const rule of stringRules) { - if (!rule.find || !rule.replace) continue; + if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, ""); @@ -222,7 +222,7 @@ function applyRules(content: string): string { if (regexRules) { for (const rule of regexRules) { - if (!rule.find || !rule.replace) continue; + if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; try { From d73790efb392b09371f0a0e7120098ee23e86a58 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 6 Jan 2024 01:22:04 +0100 Subject: [PATCH 016/356] bump to v1.6.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b59fbafe7..0b373008e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.5", + "version": "1.6.6", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From ba2695b499d511124fbc16e511ceab06c8ec1b78 Mon Sep 17 00:00:00 2001 From: Grzesiek11 Date: Sat, 6 Jan 2024 03:05:23 +0100 Subject: [PATCH 017/356] Add FixCodeblockGap plugin (#2064) --- src/plugins/fixCodeblockGap/index.ts | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/plugins/fixCodeblockGap/index.ts diff --git a/src/plugins/fixCodeblockGap/index.ts b/src/plugins/fixCodeblockGap/index.ts new file mode 100644 index 000000000..133409959 --- /dev/null +++ b/src/plugins/fixCodeblockGap/index.ts @@ -0,0 +1,35 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "FixCodeblockGap", + description: "Removes the gap between codeblocks and text below it", + authors: [Devs.Grzesiek11], + patches: [ + { + find: ".default.Messages.DELETED_ROLE_PLACEHOLDER", + replacement: { + match: String.raw`/^${"```"}(?:([a-z0-9_+\-.#]+?)\n)?\n*([^\n][^]*?)\n*${"```"}`, + replace: "$&\\n?", + }, + }, + ], +}); From ba6d23a31f61841e190e771b1ccc4d5d18e666db Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 9 Jan 2024 17:39:38 +0100 Subject: [PATCH 018/356] fix adding View Raw to undesired channel context menus --- src/plugins/permissionsViewer/index.tsx | 4 +++- src/plugins/viewRaw/index.tsx | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 985b23842..9e0131e64 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -126,7 +126,9 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback { return (children, props) => () => { - if (!props || (type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild)) return children; + if (!props) return; + if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild))) + return children; const group = findGroupChildrenByChildId(childId, children); diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index f516b5d7a..187dbc4e3 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -117,22 +117,26 @@ const settings = definePluginSettings({ } }); -function MakeContextCallback(name: string) { +function MakeContextCallback(name: "Guild" | "User" | "Channel") { const callback: NavContextMenuPatchCallback = (children, props) => () => { - if ((name === "Guild" && !props.guild) || (name === "User" && !props.user)) return; + const value = props[name.toLowerCase()]; + if (!value) return; + if (props.label === "Channel Actions") return; // random shit like notification settings + const lastChild = children.at(-1); if (lastChild?.key === "developer-actions") { const p = lastChild.props; if (!Array.isArray(p.children)) p.children = [p.children]; - ({ children } = p); + + children = p.children; } children.splice(-1, 0, openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)} + action={() => openViewRawModal(JSON.stringify(value, null, 4), name)} icon={CopyIcon} /> ); From 8c890028672be7623949ab1aa45bc5796c773a48 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 9 Jan 2024 17:44:18 +0100 Subject: [PATCH 019/356] forgor not everyone uses enlgihs --- src/plugins/viewRaw/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 187dbc4e3..08acdc4c5 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -27,7 +27,7 @@ import { Margins } from "@utils/margins"; import { copyWithToast } from "@utils/misc"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common"; +import { Button, ChannelStore, Forms, i18n, Menu, Text } from "@webpack/common"; import { Message } from "discord-types/general"; @@ -121,7 +121,7 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel") { const callback: NavContextMenuPatchCallback = (children, props) => () => { const value = props[name.toLowerCase()]; if (!value) return; - if (props.label === "Channel Actions") return; // random shit like notification settings + if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings const lastChild = children.at(-1); if (lastChild?.key === "developer-actions") { From cb7045c00bb7bbeba4b70d3a1806069878a456f3 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 13 Jan 2024 19:05:01 +0100 Subject: [PATCH 020/356] WebContextMenus: use vesktop native clipboard - fixes some permission issues --- src/plugins/webContextMenus.web/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index 50a1b90a7..bb98c61d7 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -182,6 +182,12 @@ export default definePlugin({ ], async copyImage(url: string) { + if (IS_VESKTOP && VesktopNative.clipboard) { + const data = await fetch(url).then(r => r.arrayBuffer()); + VesktopNative.clipboard.copyImage(data, url); + return; + } + // Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png // via canvas first const img = new Image(); From 69a4d2734efcfd4e1ceed18b61e56d91ec446876 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 13 Jan 2024 19:08:11 +0100 Subject: [PATCH 021/356] fix(git updater): correctly persist isDev switch --- src/main/updater/git.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts index e96026443..2ff3ba512 100644 --- a/src/main/updater/git.ts +++ b/src/main/updater/git.ts @@ -73,6 +73,8 @@ async function build() { const command = isFlatpak ? "flatpak-spawn" : "node"; const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"]; + if (IS_DEV) args.push("--dev"); + const res = await execFile(command, args, opts); return !res.stderr.includes("Build failed"); From 2ab1c50c7355849e42ad81afcfef3d008b459247 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 13 Jan 2024 19:27:41 +0100 Subject: [PATCH 022/356] QuickCss: reopen existing window instead of new one --- src/main/ipcMain.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 6254bc826..47d400eb6 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -139,8 +139,15 @@ export function initIpc(mainWindow: BrowserWindow) { } ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { + const title = "Vencord QuickCSS Editor"; + const existingWindow = BrowserWindow.getAllWindows().find(w => w.title === title); + if (existingWindow && !existingWindow.isDestroyed()) { + existingWindow.focus(); + return; + } + const win = new BrowserWindow({ - title: "Vencord QuickCSS Editor", + title, autoHideMenuBar: true, darkTheme: true, webPreferences: { From 8bd54173dbb1af446e1d710dd65dd3d67f68fc0e Mon Sep 17 00:00:00 2001 From: Manti <67705577+mantikafasi@users.noreply.github.com> Date: Mon, 15 Jan 2024 00:46:24 +0300 Subject: [PATCH 023/356] Bring back ReviewDB (#2097) Changes from old version: - You can now delete reviews on your own profile - You can now block up to 50 users. This will prevent them from leaving reviews on your profile Co-authored-by: V --- src/plugins/reviewDB/auth.tsx | 78 +++++++ .../reviewDB/components/BlockedUserModal.tsx | 99 +++++++++ .../reviewDB/components/MessageButton.tsx | 85 ++++++++ .../reviewDB/components/ReviewBadge.tsx | 50 +++++ .../reviewDB/components/ReviewComponent.tsx | 188 ++++++++++++++++ .../reviewDB/components/ReviewModal.tsx | 105 +++++++++ .../reviewDB/components/ReviewsView.tsx | 200 ++++++++++++++++++ src/plugins/reviewDB/entities.ts | 100 +++++++++ src/plugins/reviewDB/index.tsx | 157 ++++++++++++++ src/plugins/reviewDB/reviewDbApi.ts | 197 +++++++++++++++++ src/plugins/reviewDB/settings.tsx | 93 ++++++++ src/plugins/reviewDB/style.css | 121 +++++++++++ src/plugins/reviewDB/utils.tsx | 43 ++++ 13 files changed, 1516 insertions(+) create mode 100644 src/plugins/reviewDB/auth.tsx create mode 100644 src/plugins/reviewDB/components/BlockedUserModal.tsx create mode 100644 src/plugins/reviewDB/components/MessageButton.tsx create mode 100644 src/plugins/reviewDB/components/ReviewBadge.tsx create mode 100644 src/plugins/reviewDB/components/ReviewComponent.tsx create mode 100644 src/plugins/reviewDB/components/ReviewModal.tsx create mode 100644 src/plugins/reviewDB/components/ReviewsView.tsx create mode 100644 src/plugins/reviewDB/entities.ts create mode 100644 src/plugins/reviewDB/index.tsx create mode 100644 src/plugins/reviewDB/reviewDbApi.ts create mode 100644 src/plugins/reviewDB/settings.tsx create mode 100644 src/plugins/reviewDB/style.css create mode 100644 src/plugins/reviewDB/utils.tsx diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx new file mode 100644 index 000000000..1d95e47dd --- /dev/null +++ b/src/plugins/reviewDB/auth.tsx @@ -0,0 +1,78 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { DataStore } from "@api/index"; +import { Logger } from "@utils/Logger"; +import { openModal } from "@utils/modal"; +import { findByPropsLazy } from "@webpack"; +import { showToast, Toasts, UserStore } from "@webpack/common"; + +import { ReviewDBAuth } from "./entities"; + +const DATA_STORE_KEY = "rdb-auth"; + +const OAuth = findByPropsLazy("OAuth2AuthorizeModal"); + +export let Auth: ReviewDBAuth = {}; + +export async function initAuth() { + Auth = await getAuth() ?? {}; +} + +export async function getAuth(): Promise { + const auth = await DataStore.get(DATA_STORE_KEY); + return auth?.[UserStore.getCurrentUser()?.id]; +} + +export async function getToken() { + const auth = await getAuth(); + return auth?.token; +} + +export async function updateAuth(newAuth: ReviewDBAuth) { + return DataStore.update(DATA_STORE_KEY, auth => { + auth ??= {}; + Auth = auth[UserStore.getCurrentUser().id] ??= {}; + + if (newAuth.token) Auth.token = newAuth.token; + if (newAuth.user) Auth.user = newAuth.user; + + return auth; + }); +} + +export function authorize(callback?: any) { + openModal(props => + { + try { + const url = new URL(response.location); + url.searchParams.append("clientMod", "vencord"); + const res = await fetch(url, { + headers: new Headers({ Accept: "application/json" }) + }); + const { token, success } = await res.json(); + if (success) { + updateAuth({ token }); + showToast("Successfully logged in!"); + callback?.(); + } else if (res.status === 1) { + showToast("An Error occurred while logging in.", Toasts.Type.FAILURE); + } + } catch (e) { + new Logger("ReviewDB").error("Failed to authorize", e); + } + }} + /> + ); +} diff --git a/src/plugins/reviewDB/components/BlockedUserModal.tsx b/src/plugins/reviewDB/components/BlockedUserModal.tsx new file mode 100644 index 000000000..43c81eb52 --- /dev/null +++ b/src/plugins/reviewDB/components/BlockedUserModal.tsx @@ -0,0 +1,99 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; +import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal"; +import { useAwaiter } from "@utils/react"; +import { Forms, Tooltip, useState } from "@webpack/common"; + +import { Auth } from "../auth"; +import { ReviewDBUser } from "../entities"; +import { fetchBlocks, unblockUser } from "../reviewDbApi"; +import { cl } from "../utils"; + +function UnblockButton(props: { onClick?(): void; }) { + return ( + + {tooltipProps => ( +
+ + + +
+ )} +
+ ); +} + +function BlockedUser({ user, isBusy, setIsBusy }: { user: ReviewDBUser; isBusy: boolean; setIsBusy(v: boolean): void; }) { + const [gone, setGone] = useState(false); + if (gone) return null; + + return ( +
+ + {user.username} + { + setIsBusy(true); + try { + await unblockUser(user.discordID); + setGone(true); + } finally { + setIsBusy(false); + } + }} + /> +
+ ); +} + +function Modal() { + const [isBusy, setIsBusy] = useState(false); + const [blocks, error, pending] = useAwaiter(fetchBlocks, { + onError: e => new Logger("ReviewDB").error("Failed to fetch blocks", e), + fallbackValue: [], + }); + + if (pending) + return null; + if (error) + return Failed to fetch blocks: ${String(error)}; + if (!blocks.length) + return No blocked users.; + + return ( + <> + {blocks.map(b => ( + + ))} + + ); +} + +export function openBlockModal() { + openModal(modalProps => ( + + + Blocked Users + + + + {Auth.token ? : You are not logged into ReviewDB!} + + + )); +} diff --git a/src/plugins/reviewDB/components/MessageButton.tsx b/src/plugins/reviewDB/components/MessageButton.tsx new file mode 100644 index 000000000..9b0b4be1a --- /dev/null +++ b/src/plugins/reviewDB/components/MessageButton.tsx @@ -0,0 +1,85 @@ +/* + * 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 . +*/ + +import { DeleteIcon } from "@components/Icons"; +import { classes } from "@utils/misc"; +import { findByPropsLazy } from "@webpack"; +import { Tooltip } from "@webpack/common"; + +const iconClasses = findByPropsLazy("button", "wrapper", "disabled", "separator"); + +export function DeleteButton({ onClick }: { onClick(): void; }) { + return ( + + {props => ( +
+ +
+ )} +
+ ); +} + +export function ReportButton({ onClick }: { onClick(): void; }) { + return ( + + {props => ( +
+ + + +
+ )} +
+ ); +} + +export function BlockButton({ onClick, isBlocked }: { onClick(): void; isBlocked: boolean; }) { + return ( + + {props => ( +
+ + {isBlocked + ? + : + } + +
+ )} +
+ ); +} diff --git a/src/plugins/reviewDB/components/ReviewBadge.tsx b/src/plugins/reviewDB/components/ReviewBadge.tsx new file mode 100644 index 000000000..665b9bb09 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewBadge.tsx @@ -0,0 +1,50 @@ +/* + * 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 . +*/ + +import { MaskedLink, React, Tooltip } from "@webpack/common"; +import { HTMLAttributes } from "react"; + +import { Badge } from "../entities"; +import { cl } from "../utils"; + +export default function ReviewBadge(badge: Badge & { onClick?(): void; }) { + const Wrapper = badge.redirectURL + ? MaskedLink + : (props: HTMLAttributes) => ( + {props.children} + ); + + return ( + + {({ onMouseEnter, onMouseLeave }) => ( + + {badge.description} + + )} + + ); +} diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx new file mode 100644 index 000000000..5f3d135b4 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewComponent.tsx @@ -0,0 +1,188 @@ +/* + * 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 . +*/ + +import { openUserProfile } from "@utils/discord"; +import { classes } from "@utils/misc"; +import { LazyComponent } from "@utils/react"; +import { filters, findBulk } from "@webpack"; +import { Alerts, moment, Parser, showToast, Timestamp } from "@webpack/common"; + +import { Auth, getToken } from "../auth"; +import { Review, ReviewType } from "../entities"; +import { blockUser, deleteReview, reportReview, unblockUser } from "../reviewDbApi"; +import { settings } from "../settings"; +import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl } from "../utils"; +import { openBlockModal } from "./BlockedUserModal"; +import { BlockButton, DeleteButton, ReportButton } from "./MessageButton"; +import ReviewBadge from "./ReviewBadge"; + +export default LazyComponent(() => { + // this is terrible, blame ven + const p = filters.byProps; + const [ + { cozyMessage, buttons, message, buttonsInner, groupStart }, + { container, isHeader }, + { avatar, clickable, username, wrapper, cozy }, + buttonClasses, + botTag + ] = findBulk( + p("cozyMessage"), + p("container", "isHeader"), + p("avatar", "zalgo"), + p("button", "wrapper", "selected"), + p("botTag", "botTagRegular") + ); + + const dateFormat = new Intl.DateTimeFormat(); + + return function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) { + function openModal() { + openUserProfile(review.sender.discordID); + } + + function delReview() { + Alerts.show({ + title: "Are you sure?", + body: "Do you really want to delete this review?", + confirmText: "Delete", + cancelText: "Nevermind", + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to delete reviews."); + } else { + deleteReview(review.id).then(res => { + if (res.success) { + refetch(); + } + showToast(res.message); + }); + } + } + }); + } + + function reportRev() { + Alerts.show({ + title: "Are you sure?", + body: "Do you really you want to report this review?", + confirmText: "Report", + cancelText: "Nevermind", + // confirmColor: "red", this just adds a class name and breaks the submit button guh + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to report reviews."); + } else { + reportReview(review.id); + } + } + }); + } + + const isAuthorBlocked = Auth?.user?.blockedUsers?.includes(review.sender.discordID) ?? false; + + function blockReviewSender() { + if (isAuthorBlocked) + return unblockUser(review.sender.discordID); + + Alerts.show({ + title: "Are you sure?", + body: "Do you really you want to block this user? They will be unable to leave further reviews on your profile. You can unblock users in the plugin settings.", + confirmText: "Block", + cancelText: "Nevermind", + // confirmColor: "red", this just adds a class name and breaks the submit button guh + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to block users."); + } else { + blockUser(review.sender.discordID); + } + } + }); + } + + return ( +
+ + +
+ openModal()} + > + {review.sender.username} + + + {review.type === ReviewType.System && ( + + + System + + + )} +
+ {isAuthorBlocked && ( + openBlockModal()} + /> + )} + {review.sender.badges.map(badge => )} + + { + !settings.store.hideTimestamps && review.type !== ReviewType.System && ( + + {dateFormat.format(review.timestamp * 1000)} + ) + } + +
+ {Parser.parseGuildEventDescription(review.comment)} +
+ + {review.id !== 0 && ( +
+
+ {canReportReview(review) && } + {canBlockReviewAuthor(profileId, review) && } + {canDeleteReview(profileId, review) && } +
+
+ )} +
+ ); + }; +}); diff --git a/src/plugins/reviewDB/components/ReviewModal.tsx b/src/plugins/reviewDB/components/ReviewModal.tsx new file mode 100644 index 000000000..9669a2b32 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewModal.tsx @@ -0,0 +1,105 @@ +/* + * 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 . +*/ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { useForceUpdater } from "@utils/react"; +import { Paginator, Text, useRef, useState } from "@webpack/common"; + +import { Auth } from "../auth"; +import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; +import { cl } from "../utils"; +import ReviewComponent from "./ReviewComponent"; +import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; + +function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: string; name: string; }) { + const [data, setData] = useState(); + const [signal, refetch] = useForceUpdater(true); + const [page, setPage] = useState(1); + + const ref = useRef(null); + + const reviewCount = data?.reviewCount; + const ownReview = data?.reviews.find(r => r.sender.discordID === Auth.user?.discordID); + + return ( + + + + + {name}'s Reviews + {!!reviewCount && ({reviewCount} Reviews)} + + + + + +
+ ref.current?.scrollTo({ top: 0, behavior: "smooth" })} + hideOwnReview + /> +
+
+ + +
+ {ownReview && ( + + )} + + + {!!reviewCount && ( + + )} +
+
+
+
+ ); +} + +export function openReviewsModal(discordId: string, name: string) { + openModal(props => ( + + )); +} diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx new file mode 100644 index 000000000..abb856b99 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -0,0 +1,200 @@ +/* + * 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 . +*/ + +import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react"; +import { find, findByPropsLazy } from "@webpack"; +import { Forms, React, RelationshipStore, showToast, useRef, UserStore } from "@webpack/common"; + +import { Auth, authorize } from "../auth"; +import { Review } from "../entities"; +import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; +import { settings } from "../settings"; +import { cl } from "../utils"; +import ReviewComponent from "./ReviewComponent"; + + +const Slate = findByPropsLazy("Editor", "Transforms"); +const InputTypes = findByPropsLazy("ChatInputTypes"); + +const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); + +interface UserProps { + discordId: string; + name: string; +} + +interface Props extends UserProps { + onFetchReviews(data: Response): void; + refetchSignal?: unknown; + showInput?: boolean; + page?: number; + scrollToTop?(): void; + hideOwnReview?: boolean; +} + +export default function ReviewsView({ + discordId, + name, + onFetchReviews, + refetchSignal, + scrollToTop, + page = 1, + showInput = false, + hideOwnReview = false, +}: Props) { + const [signal, refetch] = useForceUpdater(true); + + const [reviewData] = useAwaiter(() => getReviews(discordId, (page - 1) * REVIEWS_PER_PAGE), { + fallbackValue: null, + deps: [refetchSignal, signal, page], + onSuccess: data => { + if (settings.store.hideBlockedUsers) + data!.reviews = data!.reviews?.filter(r => !RelationshipStore.isBlocked(r.sender.discordID)); + + scrollToTop?.(); + onFetchReviews(data!); + } + }); + + if (!reviewData) return null; + + return ( + <> + + + {showInput && ( + r.sender.discordID === UserStore.getCurrentUser().id)} + /> + )} + + ); +} + +function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; }) { + const myId = UserStore.getCurrentUser().id; + + return ( +
+ {reviews?.map(review => + (review.sender.discordID !== myId || !hideOwnReview) && + + )} + + {reviews?.length === 0 && ( + + Looks like nobody reviewed this user yet. You could be the first! + + )} +
+ ); +} + + +export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) { + const { token } = Auth; + const editorRef = useRef(null); + const inputType = InputTypes.ChatInputTypes.FORM; + inputType.disableAutoFocus = true; + + const channel = { + flags_: 256, + guild_id_: null, + id: "0", + getGuildId: () => null, + isPrivate: () => true, + isActiveThread: () => false, + isArchivedLockedThread: () => false, + isDM: () => true, + roles: { "0": { permissions: 0n } }, + getRecipientId: () => "0", + hasFlag: () => false, + }; + + return ( + <> +
{ + if (!token) { + showToast("Opening authorization window..."); + authorize(); + } + }}> + editorRef.current = ref} + textValue="" + onSubmit={ + async res => { + const response = await addReview({ + userid: discordId, + comment: res.value, + }); + + if (response?.success) { + refetch(); + + const slateEditor = editorRef.current.ref.current.getSlateEditor(); + const { Editor, Transform } = Slate; + + // clear editor + Transform.delete(slateEditor, { + at: { + anchor: Editor.start(slateEditor, []), + focus: Editor.end(slateEditor, []), + } + }); + } else if (response?.message) { + showToast(response.message); + } + + // even tho we need to return this, it doesnt do anything + return { + shouldClear: false, + shouldRefocus: true, + }; + } + } + /> +
+ + + ); +} diff --git a/src/plugins/reviewDB/entities.ts b/src/plugins/reviewDB/entities.ts new file mode 100644 index 000000000..a871d90f2 --- /dev/null +++ b/src/plugins/reviewDB/entities.ts @@ -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 . +*/ + +export const enum UserType { + Banned = -1, + Normal = 0, + Admin = 1 +} + +export const enum ReviewType { + User = 0, + Server = 1, + Support = 2, + System = 3 +} + +export const enum NotificationType { + Info = 0, + Ban = 1, + Unban = 2, + Warning = 3 +} + +export interface ReviewDBAuth { + token?: string; + user?: ReviewDBCurrentUser; +} + +export interface Badge { + name: string; + description: string; + icon: string; + redirectURL?: string; + type: number; +} + +export interface BanInfo { + id: string; + discordID: string; + reviewID: number; + reviewContent: string; + banEndDate: number; +} + +export interface Notification { + id: number; + title: string; + content: string; + type: NotificationType; +} + +export interface ReviewDBUser { + ID: number; + discordID: string; + username: string; + type: UserType; + profilePhoto: string; + badges: any[]; +} + +export interface ReviewDBCurrentUser extends ReviewDBUser { + warningCount: number; + clientMod: string; + banInfo: BanInfo | null; + notification: Notification | null; + lastReviewID: number; + blockedUsers?: string[]; +} + +export interface ReviewAuthor { + id: number, + discordID: string, + username: string, + profilePhoto: string, + badges: Badge[]; +} + +export interface Review { + comment: string, + id: number, + star: number, + sender: ReviewAuthor, + timestamp: number; + type?: ReviewType; +} diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx new file mode 100644 index 000000000..d8357faf2 --- /dev/null +++ b/src/plugins/reviewDB/index.tsx @@ -0,0 +1,157 @@ +/* + * 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 . +*/ + +import "./style.css"; + +import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import ErrorBoundary from "@components/ErrorBoundary"; +import ExpandableHeader from "@components/ExpandableHeader"; +import { OpenExternalIcon } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin from "@utils/types"; +import { Alerts, Menu, Parser, showToast, useState } from "@webpack/common"; +import { Guild, User } from "discord-types/general"; + +import { Auth, initAuth, updateAuth } from "./auth"; +import { openReviewsModal } from "./components/ReviewModal"; +import ReviewsView from "./components/ReviewsView"; +import { NotificationType } from "./entities"; +import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; +import { settings } from "./settings"; + +const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => { + children.push( + openReviewsModal(props.guild.id, props.guild.name)} + /> + ); +}; + +export default definePlugin({ + name: "ReviewDB", + description: "Review other users (Adds a new settings to profiles)", + authors: [Devs.mantikafasi, Devs.Ven], + + settings, + + patches: [ + { + find: "showBorder:null", + replacement: { + match: /user:(\i),setNote:\i,canDM.+?\}\)/, + replace: "$&,$self.getReviewsComponent($1)" + } + } + ], + + flux: { + CONNECTION_OPEN: initAuth, + }, + + async start() { + addContextMenuPatch("guild-header-popout", guildPopoutPatch); + + const s = settings.store; + const { lastReviewId, notifyReviews } = s; + + const legacy = s as any as { token?: string; }; + if (legacy.token) { + await updateAuth({ token: legacy.token }); + legacy.token = undefined; + new Logger("ReviewDB").info("Migrated legacy settings"); + } + + await initAuth(); + + setTimeout(async () => { + if (!Auth.token) return; + + const user = await getCurrentUserInfo(Auth.token); + updateAuth({ user }); + + if (notifyReviews) { + if (lastReviewId && lastReviewId < user.lastReviewID) { + s.lastReviewId = user.lastReviewID; + if (user.lastReviewID !== 0) + showToast("You have new reviews on your profile!"); + } + } + + if (user.notification) { + const props = user.notification.type === NotificationType.Ban ? { + cancelText: "Appeal", + confirmText: "Ok", + onCancel: async () => + VencordNative.native.openExternal( + "https://reviewdb.mantikafasi.dev/api/redirect?" + + new URLSearchParams({ + token: Auth.token!, + page: "dashboard/appeal" + }) + ) + } : {}; + + Alerts.show({ + title: user.notification.title, + body: ( + Parser.parse( + user.notification.content, + false + ) + ), + ...props + }); + + readNotification(user.notification.id); + } + }, 4000); + }, + + stop() { + removeContextMenuPatch("guild-header-popout", guildPopoutPatch); + }, + + getReviewsComponent: ErrorBoundary.wrap((user: User) => { + const [reviewCount, setReviewCount] = useState(); + + return ( + openReviewsModal(user.id, user.username)} + moreTooltipText={ + reviewCount && reviewCount > 50 + ? `View all ${reviewCount} reviews` + : "Open Review Modal" + } + onDropDownClick={state => settings.store.reviewsDropdownState = !state} + defaultState={settings.store.reviewsDropdownState} + > + setReviewCount(r.reviewCount)} + showInput + /> + + ); + }, { message: "Failed to render Reviews" }) +}); diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts new file mode 100644 index 000000000..a87fbcb8f --- /dev/null +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -0,0 +1,197 @@ +/* + * 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 . +*/ + +import { showToast, Toasts } from "@webpack/common"; + +import { Auth, authorize, getToken, updateAuth } from "./auth"; +import { Review, ReviewDBCurrentUser, ReviewDBUser } from "./entities"; +import { settings } from "./settings"; + +const API_URL = "https://manti.vendicated.dev"; + +export const REVIEWS_PER_PAGE = 50; + +export interface Response { + success: boolean, + message: string; + reviews: Review[]; + updated: boolean; + hasNextPage: boolean; + reviewCount: number; +} + +const WarningFlag = 0b00000010; + +export async function getReviews(id: string, offset = 0): Promise { + let flags = 0; + if (!settings.store.showWarning) flags |= WarningFlag; + + const params = new URLSearchParams({ + flags: String(flags), + offset: String(offset) + }); + const req = await fetch(`${API_URL}/api/reviewdb/users/${id}/reviews?${params}`); + + const res = (req.status === 200) + ? await req.json() as Response + : { + success: false, + message: "An Error occured while fetching reviews. Please try again later.", + reviews: [], + updated: false, + hasNextPage: false, + reviewCount: 0 + }; + + if (!res.success) { + showToast(res.message, Toasts.Type.FAILURE); + return { + ...res, + reviews: [ + { + id: 0, + comment: "An Error occured while fetching reviews. Please try again later.", + star: 0, + timestamp: 0, + sender: { + id: 0, + username: "Error", + profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128", + discordID: "0", + badges: [] + } + } + ] + }; + } + + return res; +} + +export async function addReview(review: any): Promise { + review.token = await getToken(); + + if (!review.token) { + showToast("Please authorize to add a review."); + authorize(); + return null; + } + + return fetch(API_URL + `/api/reviewdb/users/${review.userid}/reviews`, { + method: "PUT", + body: JSON.stringify(review), + headers: { + "Content-Type": "application/json", + } + }) + .then(r => r.json()) + .then(res => { + showToast(res.message); + return res ?? null; + }); +} + +export async function deleteReview(id: number): Promise { + return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, { + method: "DELETE", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + }), + body: JSON.stringify({ + token: await getToken(), + reviewid: id + }) + }).then(r => r.json()); +} + +export async function reportReview(id: number) { + const res = await fetch(API_URL + "/api/reviewdb/reports", { + method: "PUT", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + }), + body: JSON.stringify({ + reviewid: id, + token: await getToken() + }) + }).then(r => r.json()) as Response; + + showToast(res.message); +} + +async function patchBlock(action: "block" | "unblock", userId: string) { + const res = await fetch(API_URL + "/api/reviewdb/blocks", { + method: "PATCH", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + Authorization: await getToken() || "" + }), + body: JSON.stringify({ + action: action, + discordId: userId + }) + }); + + if (!res.ok) { + showToast(`Failed to ${action} user`, Toasts.Type.FAILURE); + } else { + showToast(`Successfully ${action}ed user`, Toasts.Type.SUCCESS); + + if (Auth?.user?.blockedUsers) { + const newBlockedUsers = action === "block" + ? [...Auth.user.blockedUsers, userId] + : Auth.user.blockedUsers.filter(id => id !== userId); + updateAuth({ user: { ...Auth.user, blockedUsers: newBlockedUsers } }); + } + } +} + +export const blockUser = (userId: string) => patchBlock("block", userId); +export const unblockUser = (userId: string) => patchBlock("unblock", userId); + +export async function fetchBlocks(): Promise { + const res = await fetch(API_URL + "/api/reviewdb/blocks", { + method: "GET", + headers: new Headers({ + Accept: "application/json", + Authorization: await getToken() || "" + }) + }); + + if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`); + return res.json(); +} + +export function getCurrentUserInfo(token: string): Promise { + return fetch(API_URL + "/api/reviewdb/users", { + body: JSON.stringify({ token }), + method: "POST", + }).then(r => r.json()); +} + +export async function readNotification(id: number) { + return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, { + method: "PATCH", + headers: { + "Authorization": await getToken() || "", + }, + }); +} diff --git a/src/plugins/reviewDB/settings.tsx b/src/plugins/reviewDB/settings.tsx new file mode 100644 index 000000000..efcb80588 --- /dev/null +++ b/src/plugins/reviewDB/settings.tsx @@ -0,0 +1,93 @@ +/* + * 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 . +*/ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; +import { Button } from "@webpack/common"; + +import { authorize, getToken } from "./auth"; +import { openBlockModal } from "./components/BlockedUserModal"; + +export const settings = definePluginSettings({ + authorize: { + type: OptionType.COMPONENT, + description: "Authorize with ReviewDB", + component: () => ( + + ) + }, + notifyReviews: { + type: OptionType.BOOLEAN, + description: "Notify about new reviews on startup", + default: true, + }, + showWarning: { + type: OptionType.BOOLEAN, + description: "Display warning to be respectful at the top of the reviews list", + default: true, + }, + hideTimestamps: { + type: OptionType.BOOLEAN, + description: "Hide timestamps on reviews", + default: false, + }, + hideBlockedUsers: { + type: OptionType.BOOLEAN, + description: "Hide reviews from blocked users", + default: true, + }, + manageBlocks: { + type: OptionType.COMPONENT, + description: "Manage Blocked Users", + component: () => ( + + ) + }, + website: { + type: OptionType.COMPONENT, + description: "ReviewDB website", + component: () => ( + + ) + }, + supportServer: { + type: OptionType.COMPONENT, + description: "ReviewDB Support Server", + component: () => ( + + ) + } +}).withPrivateSettings<{ + lastReviewId?: number; + reviewsDropdownState?: boolean; +}>(); diff --git a/src/plugins/reviewDB/style.css b/src/plugins/reviewDB/style.css new file mode 100644 index 000000000..a812ecaf2 --- /dev/null +++ b/src/plugins/reviewDB/style.css @@ -0,0 +1,121 @@ +[class|="section"]:not([class|="lastSection"]) + .vc-rdb-view { + margin-top: 12px; +} + +.vc-rdb-badge { + vertical-align: middle; + margin-left: 4px; +} + +.vc-rdb-input { + margin-top: 6px; + margin-bottom: 12px; + resize: none; + overflow: hidden; + background: transparent; + border: 1px solid var(--profile-message-input-border-color); +} + +.vc-rdb-modal-footer > div { + width: 100%; + margin: 6px 16px; +} + +/* When input becomes disabled(while sending review), input adds unneccesary padding to left, this prevents it */ +.vc-rdb-input > div > div { + padding-left: 0 !important; +} + +.vc-rdb-placeholder { + margin-bottom: 4px; + font-weight: bold; + font-style: italic; + color: var(--text-muted); +} + +.vc-rdb-input * { + font-size: 14px; +} + +.vc-rdb-modal-footer { + padding: 0; +} + +.vc-rdb-modal-footer .vc-rdb-input { + margin-bottom: 0; + background: var(--input-background); +} + +.vc-rdb-modal-footer [class|="pageControlContainer"] { + margin-top: 0; +} + +.vc-rdb-modal-header { + flex-grow: 1; +} + +.vc-rdb-modal-reviews { + margin-top: 16px; +} + +.vc-rdb-review { + margin-top: 8px; + margin-bottom: 8px; +} + +.vc-rdb-review-comment img { + vertical-align: text-top; +} + +.vc-rdb-review-comment { + overflow-y: hidden; + margin-top: 1px; + margin-bottom: 8px; + color: var(--text-normal); + font-size: 15px; +} + +.vc-rdb-blocked-badge { + cursor: pointer; +} + +.vc-rdb-block-modal-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.vc-rdb-block-modal { + padding: 1em; + display: grid; + gap: 0.75em; +} + +.vc-rdb-block-modal-row { + display: flex; + height: 2em; + gap: 0.5em; + align-items: center; +} + +.vc-rdb-block-modal-row img { + border-radius: 50%; + height: 2em; + width: 2em; +} + +.vc-rdb-block-modal img::before { + content: ""; + display: block; + width: 100%; + height: 100%; + background-color: var(--background-modifier-accent); +} + +.vc-rdb-block-modal-username { + flex-grow: 1; +} + +.vc-rdb-block-modal-unblock { + cursor: pointer; +} diff --git a/src/plugins/reviewDB/utils.tsx b/src/plugins/reviewDB/utils.tsx new file mode 100644 index 000000000..eeaca1204 --- /dev/null +++ b/src/plugins/reviewDB/utils.tsx @@ -0,0 +1,43 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { classNameFactory } from "@api/Styles"; +import { UserStore } from "@webpack/common"; + +import { Auth } from "./auth"; +import { Review, UserType } from "./entities"; + +export const cl = classNameFactory("vc-rdb-"); + +export function canDeleteReview(profileId: string, review: Review) { + const myId = UserStore.getCurrentUser().id; + return ( + myId === profileId + || review.sender.discordID === myId + || Auth.user?.type === UserType.Admin + ); +} + +export function canBlockReviewAuthor(profileId: string, review: Review) { + const myId = UserStore.getCurrentUser().id; + return profileId === myId && review.sender.discordID !== myId; +} + +export function canReportReview(review: Review) { + return review.sender.discordID !== UserStore.getCurrentUser().id; +} From a171b35e97c8931e2466a414a65463f349fb7b11 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Mon, 15 Jan 2024 17:36:24 +0100 Subject: [PATCH 024/356] ReviewDB fixes --- src/plugins/reviewDB/auth.tsx | 6 ++-- .../reviewDB/components/ReviewComponent.tsx | 2 +- .../reviewDB/components/ReviewsView.tsx | 9 +++--- src/plugins/reviewDB/reviewDbApi.ts | 31 ++++++++++--------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx index 1d95e47dd..e7a369217 100644 --- a/src/plugins/reviewDB/auth.tsx +++ b/src/plugins/reviewDB/auth.tsx @@ -14,7 +14,7 @@ import { ReviewDBAuth } from "./entities"; const DATA_STORE_KEY = "rdb-auth"; -const OAuth = findByPropsLazy("OAuth2AuthorizeModal"); +const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal"); export let Auth: ReviewDBAuth = {}; @@ -46,7 +46,7 @@ export async function updateAuth(newAuth: ReviewDBAuth) { export function authorize(callback?: any) { openModal(props => - { - // this is terrible, blame ven + // this is terrible, blame mantika const p = filters.byProps; const [ { cozyMessage, buttons, message, buttonsInner, groupStart }, diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index abb856b99..cfd5477db 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -28,8 +28,8 @@ import { cl } from "../utils"; import ReviewComponent from "./ReviewComponent"; -const Slate = findByPropsLazy("Editor", "Transforms"); -const InputTypes = findByPropsLazy("ChatInputTypes"); +const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms"); +const { ChatInputTypes } = findByPropsLazy("ChatInputTypes"); const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); @@ -122,7 +122,7 @@ function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch(): export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) { const { token } = Auth; const editorRef = useRef(null); - const inputType = InputTypes.ChatInputTypes.FORM; + const inputType = ChatInputTypes.FORM; inputType.disableAutoFocus = true; const channel = { @@ -172,10 +172,9 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { refetch(); const slateEditor = editorRef.current.ref.current.getSlateEditor(); - const { Editor, Transform } = Slate; // clear editor - Transform.delete(slateEditor, { + Transforms.delete(slateEditor, { at: { anchor: Editor.start(slateEditor, []), focus: Editor.end(slateEditor, []), diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts index a87fbcb8f..657e9c475 100644 --- a/src/plugins/reviewDB/reviewDbApi.ts +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -19,10 +19,10 @@ import { showToast, Toasts } from "@webpack/common"; import { Auth, authorize, getToken, updateAuth } from "./auth"; -import { Review, ReviewDBCurrentUser, ReviewDBUser } from "./entities"; +import { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from "./entities"; import { settings } from "./settings"; -const API_URL = "https://manti.vendicated.dev"; +const API_URL = "https://manti.vendicated.dev/api/reviewdb"; export const REVIEWS_PER_PAGE = 50; @@ -45,13 +45,13 @@ export async function getReviews(id: string, offset = 0): Promise { flags: String(flags), offset: String(offset) }); - const req = await fetch(`${API_URL}/api/reviewdb/users/${id}/reviews?${params}`); + const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`); const res = (req.status === 200) ? await req.json() as Response : { success: false, - message: "An Error occured while fetching reviews. Please try again later.", + message: req.status === 429 ? "You are sending requests too fast. Wait a few seconds and try again." : "An Error occured while fetching reviews. Please try again later.", reviews: [], updated: false, hasNextPage: false, @@ -65,14 +65,15 @@ export async function getReviews(id: string, offset = 0): Promise { reviews: [ { id: 0, - comment: "An Error occured while fetching reviews. Please try again later.", + comment: res.message, star: 0, timestamp: 0, + type: ReviewType.System, sender: { id: 0, - username: "Error", - profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128", - discordID: "0", + username: "ReviewDB", + profilePhoto: "https://cdn.discordapp.com/avatars/1134864775000629298/3f87ad315b32ee464d84f1270c8d1b37.png?size=256&format=webp&quality=lossless", + discordID: "1134864775000629298", badges: [] } } @@ -92,7 +93,7 @@ export async function addReview(review: any): Promise { return null; } - return fetch(API_URL + `/api/reviewdb/users/${review.userid}/reviews`, { + return fetch(API_URL + `/users/${review.userid}/reviews`, { method: "PUT", body: JSON.stringify(review), headers: { @@ -107,7 +108,7 @@ export async function addReview(review: any): Promise { } export async function deleteReview(id: number): Promise { - return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, { + return fetch(API_URL + `/users/${id}/reviews`, { method: "DELETE", headers: new Headers({ "Content-Type": "application/json", @@ -121,7 +122,7 @@ export async function deleteReview(id: number): Promise { } export async function reportReview(id: number) { - const res = await fetch(API_URL + "/api/reviewdb/reports", { + const res = await fetch(API_URL + "/reports", { method: "PUT", headers: new Headers({ "Content-Type": "application/json", @@ -137,7 +138,7 @@ export async function reportReview(id: number) { } async function patchBlock(action: "block" | "unblock", userId: string) { - const res = await fetch(API_URL + "/api/reviewdb/blocks", { + const res = await fetch(API_URL + "/blocks", { method: "PATCH", headers: new Headers({ "Content-Type": "application/json", @@ -168,7 +169,7 @@ export const blockUser = (userId: string) => patchBlock("block", userId); export const unblockUser = (userId: string) => patchBlock("unblock", userId); export async function fetchBlocks(): Promise { - const res = await fetch(API_URL + "/api/reviewdb/blocks", { + const res = await fetch(API_URL + "/blocks", { method: "GET", headers: new Headers({ Accept: "application/json", @@ -181,14 +182,14 @@ export async function fetchBlocks(): Promise { } export function getCurrentUserInfo(token: string): Promise { - return fetch(API_URL + "/api/reviewdb/users", { + return fetch(API_URL + "/users", { body: JSON.stringify({ token }), method: "POST", }).then(r => r.json()); } export async function readNotification(id: number) { - return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, { + return fetch(API_URL + `/notifications?id=${id}`, { method: "PATCH", headers: { "Authorization": await getToken() || "", From 1a982ae9aa51081f12c34c5dd68857bfbb4f312e Mon Sep 17 00:00:00 2001 From: rini Date: Mon, 15 Jan 2024 23:51:12 -0300 Subject: [PATCH 025/356] feat(anonymisefilenames): add icon to toggle anonymising (#2087) Co-authored-by: V Co-authored-by: fawn --- src/plugins/anonymiseFileNames/index.ts | 94 ---------------- src/plugins/anonymiseFileNames/index.tsx | 130 +++++++++++++++++++++++ 2 files changed, 130 insertions(+), 94 deletions(-) delete mode 100644 src/plugins/anonymiseFileNames/index.ts create mode 100644 src/plugins/anonymiseFileNames/index.tsx diff --git a/src/plugins/anonymiseFileNames/index.ts b/src/plugins/anonymiseFileNames/index.ts deleted file mode 100644 index 9e69d7a93..000000000 --- a/src/plugins/anonymiseFileNames/index.ts +++ /dev/null @@ -1,94 +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 . -*/ - -import { Settings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const enum Methods { - Random, - Consistent, - Timestamp, -} - -const tarExtMatcher = /\.tar\.\w+$/; - -export default definePlugin({ - name: "AnonymiseFileNames", - authors: [Devs.obscurity], - description: "Anonymise uploaded file names", - patches: [ - { - find: "instantBatchUpload:function", - replacement: { - match: /uploadFiles:(.{1,2}),/, - replace: - "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),", - }, - }, - ], - - options: { - method: { - description: "Anonymising method", - type: OptionType.SELECT, - options: [ - { label: "Random Characters", value: Methods.Random, default: true }, - { label: "Consistent", value: Methods.Consistent }, - { label: "Timestamp (4chan-like)", value: Methods.Timestamp }, - ], - }, - randomisedLength: { - description: "Random characters length", - type: OptionType.NUMBER, - default: 7, - disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Random, - }, - consistent: { - description: "Consistent filename", - type: OptionType.STRING, - default: "image", - disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Consistent, - }, - }, - - anonymise(file: string) { - let name = "image"; - const tarMatch = tarExtMatcher.exec(file); - const extIdx = tarMatch?.index ?? file.lastIndexOf("."); - const ext = extIdx !== -1 ? file.slice(extIdx) : ""; - - switch (Settings.plugins.AnonymiseFileNames.method) { - case Methods.Random: - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - name = Array.from( - { length: Settings.plugins.AnonymiseFileNames.randomisedLength }, - () => chars[Math.floor(Math.random() * chars.length)] - ).join(""); - break; - case Methods.Consistent: - name = Settings.plugins.AnonymiseFileNames.consistent; - break; - case Methods.Timestamp: - // UNIX timestamp in nanos, i could not find a better dependency-less way - name = `${Math.floor(Date.now() / 1000)}${Math.floor(window.performance.now())}`; - break; - } - return name + ext; - }, -}); diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx new file mode 100644 index 000000000..845aa756d --- /dev/null +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -0,0 +1,130 @@ +/* + * 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 . +*/ + +import { Upload } from "@api/MessageEvents"; +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; + +type AnonUpload = Upload & { anonymise?: boolean; }; + +const ActionBarIcon = findByCodeLazy(".actionBarIcon)"); +const UploadDraft = findByPropsLazy("popFirstFile", "update"); + +const enum Methods { + Random, + Consistent, + Timestamp, +} + +const tarExtMatcher = /\.tar\.\w+$/; + +const settings = definePluginSettings({ + anonymiseByDefault: { + description: "Whether to anonymise file names by default", + type: OptionType.BOOLEAN, + default: true, + }, + method: { + description: "Anonymising method", + type: OptionType.SELECT, + options: [ + { label: "Random Characters", value: Methods.Random, default: true }, + { label: "Consistent", value: Methods.Consistent }, + { label: "Timestamp", value: Methods.Timestamp }, + ], + }, + randomisedLength: { + description: "Random characters length", + type: OptionType.NUMBER, + default: 7, + disabled: () => settings.store.method !== Methods.Random, + }, + consistent: { + description: "Consistent filename", + type: OptionType.STRING, + default: "image", + disabled: () => settings.store.method !== Methods.Consistent, + }, +}); + +export default definePlugin({ + name: "AnonymiseFileNames", + authors: [Devs.obscurity], + description: "Anonymise uploaded file names", + patches: [ + { + find: "instantBatchUpload:function", + replacement: { + match: /uploadFiles:(.{1,2}),/, + replace: + "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),", + }, + }, + { + find: ".Messages.ATTACHMENT_UTILITIES_SPOILER", + replacement: { + match: /(?<=children:\[)(?=.{10,80}tooltip:\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/, + replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null," + }, + }, + ], + settings, + + renderIcon: ErrorBoundary.wrap(({ upload, channelId, draftType }: { upload: AnonUpload; draftType: unknown; channelId: string; }) => { + const anonymise = upload.anonymise ?? settings.store.anonymiseByDefault; + return ( + { + upload.anonymise = !anonymise; + UploadDraft.update(channelId, upload.id, draftType, {}); // dummy update so component rerenders + }} + > + {anonymise + ? + : + } + + ); + }, { noop: true }), + + anonymise(upload: AnonUpload) { + if ((upload.anonymise ?? settings.store.anonymiseByDefault) === false) return upload.filename; + + const file = upload.filename; + const tarMatch = tarExtMatcher.exec(file); + const extIdx = tarMatch?.index ?? file.lastIndexOf("."); + const ext = extIdx !== -1 ? file.slice(extIdx) : ""; + + switch (settings.store.method) { + case Methods.Random: + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + return Array.from( + { length: settings.store.randomisedLength }, + () => chars[Math.floor(Math.random() * chars.length)] + ).join("") + ext; + case Methods.Consistent: + return settings.store.consistent + ext; + case Methods.Timestamp: + return Date.now() + ext; + } + }, +}); From f14001b531d4adc6cc97d382c481b14de274b234 Mon Sep 17 00:00:00 2001 From: CodeF53 <37855219+CodeF53@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:53:27 -0700 Subject: [PATCH 026/356] ClientTheme light mode support, shortcut buttons (#2010) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Co-authored-by: V --- src/components/VencordSettings/ThemesTab.tsx | 17 ++ src/plugins/clientTheme/clientTheme.css | 12 +- src/plugins/clientTheme/index.tsx | 209 +++++++++++++------ 3 files changed, 173 insertions(+), 65 deletions(-) diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 2808494a1..8abaaba4f 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -21,9 +21,11 @@ import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; import { DeleteIcon } from "@components/Icons"; import { Link } from "@components/Link"; +import PluginModal from "@components/PluginSettings/PluginModal"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; +import { openModal } from "@utils/modal"; import { showItemInFolder } from "@utils/native"; import { useAwaiter } from "@utils/react"; import { findByPropsLazy, findLazy } from "@webpack"; @@ -248,6 +250,21 @@ function ThemesTab() { > Edit QuickCSS + + {Vencord.Settings.plugins.ClientTheme.enabled && ( + + )} diff --git a/src/plugins/clientTheme/clientTheme.css b/src/plugins/clientTheme/clientTheme.css index 023f88bd2..64aefdcf5 100644 --- a/src/plugins/clientTheme/clientTheme.css +++ b/src/plugins/clientTheme/clientTheme.css @@ -19,6 +19,16 @@ border: thin solid var(--background-modifier-accent) !important; } -.client-theme-warning { +.client-theme-warning * { color: var(--text-danger); } + +.client-theme-contrast-warning { + background-color: var(--background-primary); + padding: 0.5rem; + border-radius: .5rem; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index d75929961..5d8cd4dc0 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -8,19 +8,19 @@ import "./clientTheme.css"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import { getTheme, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import definePlugin, { OptionType, StartAt } from "@utils/types"; -import { findComponentByCodeLazy } from "@webpack"; -import { Button, Forms } from "@webpack/common"; +import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; +import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common"; const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const colorPresets = [ "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D", "#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42", - "#3C2E42", "#422938" + "#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d", + "#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb", ]; function onPickColor(color: number) { @@ -30,9 +30,35 @@ function onPickColor(color: number) { updateColorVars(hexColor); } +const { saveClientTheme } = findByPropsLazy("saveClientTheme"); + +function setTheme(theme: string) { + saveClientTheme({ theme }); +} + +const ThemeStore = findStoreLazy("ThemeStore"); +const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore"); + function ThemeSettings() { - const lightnessWarning = hexToLightness(settings.store.color) > 45; - const lightModeWarning = getTheme() === Theme.Light; + const theme = useStateFromStores([ThemeStore], () => ThemeStore.theme); + const isLightTheme = theme === "light"; + const oppositeTheme = isLightTheme ? "dark" : "light"; + + const nitroTheme = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset); + const nitroThemeEnabled = nitroTheme !== undefined; + + const selectedLuminance = relativeLuminance(settings.store.color); + + let contrastWarning = false, fixableContrast = true; + if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12) + contrastWarning = true; + if (selectedLuminance < 0.26 && selectedLuminance > 0.12) + fixableContrast = false; + // light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels + if (isLightTheme && selectedLuminance > 0.65) { + contrastWarning = true; + fixableContrast = false; + } return (
@@ -48,15 +74,18 @@ function ThemeSettings() { suggestedColors={colorPresets} />
- {lightnessWarning || lightModeWarning - ?
- - Your theme won't look good: - {lightnessWarning && Selected color is very light} - {lightModeWarning && Light mode isn't supported} + {(contrastWarning || nitroThemeEnabled) && (<> + +
+
+ Warning, your theme won't look good: + {contrastWarning && Selected color won't contrast well with text} + {nitroThemeEnabled && Nitro themes aren't supported} +
+ {(contrastWarning && fixableContrast) && } + {(nitroThemeEnabled) && }
- : null - } + )}
); } @@ -87,9 +116,12 @@ export default definePlugin({ settings, startAt: StartAt.DOMContentLoaded, - start() { + async start() { updateColorVars(settings.store.color); - generateColorOffsets(); + + const styles = await getStyles(); + generateColorOffsets(styles); + generateLightModeFixes(styles); }, stop() { @@ -98,56 +130,86 @@ export default definePlugin({ } }); -const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g; +const variableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g; +const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g; +const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g; -async function generateColorOffsets() { - - const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); - const variableLightness = {} as Record; - - // Search all stylesheets for color variables - for (const styleLinkNode of styleLinkNodes) { - const cssLink = styleLinkNode.getAttribute("href"); - if (!cssLink) continue; - - const res = await fetch(cssLink); - const cssString = await res.text(); - - // Get lightness values of --primary variables >=500 - let variableMatch = variableRegex.exec(cssString); - while (variableMatch !== null) { - const [, variable, lightness] = variableMatch; - variableLightness[variable] = parseFloat(lightness); - variableMatch = variableRegex.exec(cssString); - } - } - - // Generate offsets - const lightnessOffsets = Object.entries(variableLightness) +// generates variables per theme by: +// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable) +// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600) +function genThemeSpecificOffsets(variableLightness: Record, regex: RegExp, centerVariable: string): string { + return Object.entries(variableLightness).filter(([key]) => key.search(regex) > -1) .map(([key, lightness]) => { - const lightnessOffset = lightness - variableLightness["--primary-600-hsl"]; + const lightnessOffset = lightness - variableLightness[centerVariable]; const plusOrMinus = lightnessOffset >= 0 ? "+" : "-"; return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`; }) .join("\n"); +} - const style = document.createElement("style"); - style.setAttribute("id", "clientThemeOffsets"); - style.textContent = `:root:root { - ${lightnessOffsets} - }`; - document.head.appendChild(style); + +function generateColorOffsets(styles) { + const variableLightness = {} as Record; + + // Get lightness values of --primary variables + let variableMatch = variableRegex.exec(styles); + while (variableMatch !== null) { + const [, variable, lightness] = variableMatch; + variableLightness[variable] = parseFloat(lightness); + variableMatch = variableRegex.exec(styles); + } + + createStyleSheet("clientThemeOffsets", [ + `.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`, + `.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`, + ].join("\n\n")); +} + +function generateLightModeFixes(styles) { + const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm; + // get light capturing groups that mention --white-500 + const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat(); + + const groupBackgroundRegex = /^([^{]*)\{background:var\(--white-500\)/m; + const groupBackgroundColorRegex = /^([^{]*)\{background-color:var\(--white-500\)/m; + // find all capturing groups that assign background or background-color directly to w500 + const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n"); + const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n"); + // create css to reassign them to --primary-100 + const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`; + const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`; + + const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m; + const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m; + // get all global variables used for backgrounds + const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500 + .map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[] + const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500 + // create css to reassign every var + const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`; + + createStyleSheet("clientThemeLightModeFixes", [ + reassignBackgrounds, + reassignBackgroundColors, + reassignVariables, + ].join("\n\n")); +} + +function captureOne(str, regex) { + const result = str.match(regex); + return (result === null) ? null : result[1]; +} + +function mapReject(arr, mapFunc, rejectFunc = _.isNull) { + return _.reject(arr.map(mapFunc), rejectFunc); } function updateColorVars(color: string) { const { hue, saturation, lightness } = hexToHSL(color); let style = document.getElementById("clientThemeVars"); - if (!style) { - style = document.createElement("style"); - style.setAttribute("id", "clientThemeVars"); - document.head.appendChild(style); - } + if (!style) + style = createStyleSheet("clientThemeVars"); style.textContent = `:root { --theme-h: ${hue}; @@ -156,6 +218,28 @@ function updateColorVars(color: string) { }`; } +function createStyleSheet(id, content = "") { + const style = document.createElement("style"); + style.setAttribute("id", id); + style.textContent = content.split("\n").map(line => line.trim()).join("\n"); + document.body.appendChild(style); + return style; +} + +// returns all of discord's native styles in a single string +async function getStyles(): Promise { + let out = ""; + const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); + for (const styleLinkNode of styleLinkNodes) { + const cssLink = styleLinkNode.getAttribute("href"); + if (!cssLink) continue; + + const res = await fetch(cssLink); + out += await res.text(); + } + return out; +} + // https://css-tricks.com/converting-color-spaces-in-javascript/ function hexToHSL(hexCode: string) { // Hex => RGB normalized to 0-1 @@ -198,17 +282,14 @@ function hexToHSL(hexCode: string) { return { hue, saturation, lightness }; } -// Minimized math just for lightness, lowers lag when changing colors -function hexToLightness(hexCode: string) { - // Hex => RGB normalized to 0-1 - const r = parseInt(hexCode.substring(0, 2), 16) / 255; - const g = parseInt(hexCode.substring(2, 4), 16) / 255; - const b = parseInt(hexCode.substring(4, 6), 16) / 255; +// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance +function relativeLuminance(hexCode: string) { + const normalize = (x: number) => + x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; - const cMax = Math.max(r, g, b); - const cMin = Math.min(r, g, b); + const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255); + const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255); + const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255); - const lightness = 100 * ((cMax + cMin) / 2); - - return lightness; + return r * 0.2126 + g * 0.7152 + b * 0.0722; } From 60bc823eab8876a63f8b3d192a703c99ace8921a Mon Sep 17 00:00:00 2001 From: Sam <149597648+cheesesamwich@users.noreply.github.com> Date: Tue, 16 Jan 2024 03:00:41 +0000 Subject: [PATCH 027/356] new plugin: BetterGifPicker (#2108) Co-authored-by: V --- src/plugins/betterGifPicker/index.ts | 23 +++++++++++++++++++++++ src/utils/constants.ts | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 src/plugins/betterGifPicker/index.ts diff --git a/src/plugins/betterGifPicker/index.ts b/src/plugins/betterGifPicker/index.ts new file mode 100644 index 000000000..09bb570d7 --- /dev/null +++ b/src/plugins/betterGifPicker/index.ts @@ -0,0 +1,23 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "BetterGifPicker", + description: "Makes the gif picker open the favourite category by default", + authors: [Devs.Samwich], + patches: [ + { + find: ".GIFPickerResultTypes.SEARCH", + replacement: [{ + match: "this.state={resultType:null}", + replace: 'this.state={resultType:"Favorites"}' + }] + } + ] +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index a94ba0fc3..699c40b90 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -403,6 +403,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Grzesiek11", id: 368475654662127616n, }, + Samwich: { + name: "Samwich", + id: 976176454511509554n, + }, } satisfies Record); // iife so #__PURE__ works correctly From 11d3165009d0a5d8a2f2594a88f60317f239b8a8 Mon Sep 17 00:00:00 2001 From: AutumnVN Date: Tue, 16 Jan 2024 10:00:53 +0700 Subject: [PATCH 028/356] roleColorEverywhere: thread role color (again) (#2098) Co-authored-by: V --- src/plugins/roleColorEverywhere/index.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index d718f4027..968027163 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -72,10 +72,6 @@ export default definePlugin({ { find: 'tutorialId:"whos-online', replacement: [ - { - match: /\i.roleIcon,\.\.\.\i/, - replace: "$&,color:$self.roleGroupColor(arguments[0])" - }, { match: /null,\i," — ",\i\]/, replace: "null,$self.roleGroupColor(arguments[0])]" @@ -83,6 +79,16 @@ export default definePlugin({ ], predicate: () => settings.store.memberList, }, + { + find: ".Messages.THREAD_BROWSER_PRIVATE", + replacement: [ + { + match: /children:\[\i," — ",\i\]/, + replace: "children:[$self.roleGroupColor(arguments[0])]" + }, + ], + predicate: () => settings.store.memberList, + }, { find: "renderPrioritySpeaker", replacement: [ From e8e09c17e9bb60b5c26ae862394d39faf5f6cd3b Mon Sep 17 00:00:00 2001 From: Bloofield <64427742+Bloofield@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:07:05 +1100 Subject: [PATCH 029/356] ClearURLs: Add igsh (#2103) --- src/plugins/clearURLs/defaultRules.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/clearURLs/defaultRules.ts b/src/plugins/clearURLs/defaultRules.ts index 0633b717d..95a59c037 100644 --- a/src/plugins/clearURLs/defaultRules.ts +++ b/src/plugins/clearURLs/defaultRules.ts @@ -153,5 +153,6 @@ export const defaultRules = [ "utm_term", "si@open.spotify.com", "igshid", + "igsh", "share_id@reddit.com", ]; From 8a168bd18571a08143bebb2bcd9c3807d19fc6d7 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 16 Jan 2024 04:12:15 +0100 Subject: [PATCH 030/356] bump to v1.6.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b373008e..076b2999d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.6", + "version": "1.6.7", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 3d64f3da410153447bc2fd49af2d7799af993e51 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 18 Jan 2024 00:39:34 +0100 Subject: [PATCH 031/356] WebContextMenus: fix copying images --- src/plugins/webContextMenus.web/index.ts | 67 ++++++++++++++++-------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index bb98c61d7..ec3070b11 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -46,6 +46,21 @@ const settings = definePluginSettings({ } }); +const MEDIA_PROXY_URL = "https://media.discordapp.net"; +const CDN_URL = "https://cdn.discordapp.com"; + +function fixImageUrl(urlString: string) { + const url = new URL(urlString); + if (url.origin === CDN_URL) return urlString; + if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname; + + url.searchParams.delete("width"); + url.searchParams.delete("height"); + url.searchParams.set("quality", "lossless"); + if (url.searchParams.get("format") === "webp") url.searchParams.set("format", "png"); + return url.toString(); +} + export default definePlugin({ name: "WebContextMenus", description: "Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)", @@ -182,34 +197,40 @@ export default definePlugin({ ], async copyImage(url: string) { - if (IS_VESKTOP && VesktopNative.clipboard) { - const data = await fetch(url).then(r => r.arrayBuffer()); - VesktopNative.clipboard.copyImage(data, url); - return; + url = fixImageUrl(url); + + let imageData = await fetch(url).then(r => r.blob()); + if (imageData.type !== "image/png") { + const bitmap = await createImageBitmap(imageData); + + const canvas = document.createElement("canvas"); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + canvas.getContext("2d")!.drawImage(bitmap, 0, 0); + + await new Promise(done => { + canvas.toBlob(data => { + imageData = data!; + done(); + }, "image/png"); + }); } - // Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png - // via canvas first - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = img.naturalWidth; - canvas.height = img.naturalHeight; - canvas.getContext("2d")!.drawImage(img, 0, 0); - - canvas.toBlob(data => { - navigator.clipboard.write([ - new ClipboardItem({ - "image/png": data! - }) - ]); - }, "image/png"); - }; - img.crossOrigin = "anonymous"; - img.src = url; + if (IS_VESKTOP && VesktopNative.clipboard) { + VesktopNative.clipboard.copyImage(await imageData.arrayBuffer(), url); + return; + } else { + navigator.clipboard.write([ + new ClipboardItem({ + "image/png": imageData + }) + ]); + } }, async saveImage(url: string) { + url = fixImageUrl(url); + const data = await fetchImage(url); if (!data) return; From 74300e0a6963dec7f83dc9f21f93736be9990b59 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 18 Jan 2024 00:58:40 +0100 Subject: [PATCH 032/356] WebContextMenus: only force png on copy image, not save image --- src/plugins/webContextMenus.web/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index ec3070b11..5f6beca2c 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -49,7 +49,7 @@ const settings = definePluginSettings({ const MEDIA_PROXY_URL = "https://media.discordapp.net"; const CDN_URL = "https://cdn.discordapp.com"; -function fixImageUrl(urlString: string) { +function fixImageUrl(urlString: string, explodeWebp: boolean) { const url = new URL(urlString); if (url.origin === CDN_URL) return urlString; if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname; @@ -57,7 +57,9 @@ function fixImageUrl(urlString: string) { url.searchParams.delete("width"); url.searchParams.delete("height"); url.searchParams.set("quality", "lossless"); - if (url.searchParams.get("format") === "webp") url.searchParams.set("format", "png"); + if (explodeWebp && url.searchParams.get("format") === "webp") + url.searchParams.set("format", "png"); + return url.toString(); } @@ -197,7 +199,7 @@ export default definePlugin({ ], async copyImage(url: string) { - url = fixImageUrl(url); + url = fixImageUrl(url, true); let imageData = await fetch(url).then(r => r.blob()); if (imageData.type !== "image/png") { @@ -229,7 +231,7 @@ export default definePlugin({ }, async saveImage(url: string) { - url = fixImageUrl(url); + url = fixImageUrl(url, false); const data = await fetchImage(url); if (!data) return; From 988435714e8b1de6e7e95885cbb5222113fb039c Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 19 Jan 2024 01:08:25 +0100 Subject: [PATCH 033/356] Add back transparency option --- src/components/VencordSettings/VencordTab.tsx | 6 +++--- src/main/patcher.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 07d777eb3..ab910ea2a 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -83,10 +83,10 @@ function VencordSettings() { title: "Use Windows' native title bar instead of Discord's custom one", note: "Requires a full restart" }), - !IS_WEB && false /* This causes electron to freeze / white screen for some people */ && { + !IS_WEB && { key: "transparent", - title: "Enable window transparency", - note: "Requires a full restart" + title: "Enable window transparency.", + note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart" }, !IS_WEB && isWindows && { key: "winCtrlQ", diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 0cc92550c..76d1ccaf3 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -79,8 +79,7 @@ if (!IS_VANILLA) { delete options.frame; } - // This causes electron to freeze / white screen for some people - if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) { + if (settings.transparent) { options.transparent = true; options.backgroundColor = "#00000000"; } From 16707334582151df832743042ba6189936048ee7 Mon Sep 17 00:00:00 2001 From: ~coolelectronics Date: Thu, 18 Jan 2024 20:07:35 -0500 Subject: [PATCH 034/356] feat(plugin): FixYoutubeEmbeds - fix UMG blocked embeds (#2116) Co-authored-by: V --- .../fixYoutubeEmbeds.desktop/README.md | 5 ++++ src/plugins/fixYoutubeEmbeds.desktop/index.ts | 14 ++++++++++ .../fixYoutubeEmbeds.desktop/native.ts | 26 +++++++++++++++++++ src/utils/constants.ts | 4 +++ 4 files changed, 49 insertions(+) create mode 100644 src/plugins/fixYoutubeEmbeds.desktop/README.md create mode 100644 src/plugins/fixYoutubeEmbeds.desktop/index.ts create mode 100644 src/plugins/fixYoutubeEmbeds.desktop/native.ts diff --git a/src/plugins/fixYoutubeEmbeds.desktop/README.md b/src/plugins/fixYoutubeEmbeds.desktop/README.md new file mode 100644 index 000000000..875de9e28 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/README.md @@ -0,0 +1,5 @@ +# FixYoutubeEmbeds + +Bypasses youtube videos being blocked from display on Discord (for example by UMG) + +![](https://github.com/Vendicated/Vencord/assets/45497981/7a5fdcaa-217c-4c63-acae-f0d6af2f79be) diff --git a/src/plugins/fixYoutubeEmbeds.desktop/index.ts b/src/plugins/fixYoutubeEmbeds.desktop/index.ts new file mode 100644 index 000000000..55486d763 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/index.ts @@ -0,0 +1,14 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "FixYoutubeEmbeds", + description: "Bypasses youtube videos being blocked from display on Discord (for example by UMG)", + authors: [Devs.coolelectronics] +}); diff --git a/src/plugins/fixYoutubeEmbeds.desktop/native.ts b/src/plugins/fixYoutubeEmbeds.desktop/native.ts new file mode 100644 index 000000000..5a3ef2c62 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/native.ts @@ -0,0 +1,26 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { app } from "electron"; +import { getSettings } from "main/ipcMain"; + +app.on("browser-window-created", (_, win) => { + win.webContents.on("frame-created", (_, { frame }) => { + frame.once("dom-ready", () => { + if (frame.url.startsWith("https://www.youtube.com/")) { + const settings = getSettings().plugins?.FixYoutubeEmbeds; + if (!settings?.enabled) return; + + frame.executeJavaScript(` + new MutationObserver(() => { + let err = document.querySelector(".ytp-error-content-wrap-subreason span")?.textContent; + if (err && err.includes("blocked it from display")) window.location.reload() + }).observe(document.body, { childList: true, subtree:true }); + `); + } + }); + }); +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 699c40b90..899936128 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -407,6 +407,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Samwich", id: 976176454511509554n, }, + coolelectronics: { + name: "coolelectronics", + id: 696392247205298207n, + } } satisfies Record); // iife so #__PURE__ works correctly From e707538b73ee8e2430c09a476904ea41ab1b7979 Mon Sep 17 00:00:00 2001 From: Manti <67705577+mantikafasi@users.noreply.github.com> Date: Mon, 22 Jan 2024 03:18:48 +0300 Subject: [PATCH 035/356] [ReviewDB] update for new api changes; some fixes (#2120) Co-authored-by: Vendicated --- src/plugins/reviewDB/auth.tsx | 17 +++-- .../reviewDB/components/ReviewComponent.tsx | 17 +++-- .../reviewDB/components/ReviewsView.tsx | 8 +-- src/plugins/reviewDB/index.tsx | 3 +- src/plugins/reviewDB/reviewDbApi.ts | 66 ++++++++++--------- src/plugins/reviewDB/settings.tsx | 63 +++++++++--------- src/plugins/reviewDB/style.css | 23 ++++++- src/plugins/reviewDB/utils.tsx | 13 +++- 8 files changed, 126 insertions(+), 84 deletions(-) diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx index e7a369217..136001b2d 100644 --- a/src/plugins/reviewDB/auth.tsx +++ b/src/plugins/reviewDB/auth.tsx @@ -61,14 +61,17 @@ export function authorize(callback?: any) { const res = await fetch(url, { headers: new Headers({ Accept: "application/json" }) }); - const { token, success } = await res.json(); - if (success) { - updateAuth({ token }); - showToast("Successfully logged in!", Toasts.Type.SUCCESS); - callback?.(); - } else if (res.status === 1) { - showToast("An Error occurred while logging in.", Toasts.Type.FAILURE); + + if (!res.ok) { + const { message } = await res.json(); + showToast(message || "An error occured while authorizing", Toasts.Type.FAILURE); + return; } + + const { token } = await res.json(); + updateAuth({ token }); + showToast("Successfully logged in!", Toasts.Type.SUCCESS); + callback?.(); } catch (e) { new Logger("ReviewDB").error("Failed to authorize", e); } diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx index 13b0f167f..977745a25 100644 --- a/src/plugins/reviewDB/components/ReviewComponent.tsx +++ b/src/plugins/reviewDB/components/ReviewComponent.tsx @@ -20,13 +20,13 @@ import { openUserProfile } from "@utils/discord"; import { classes } from "@utils/misc"; import { LazyComponent } from "@utils/react"; import { filters, findBulk } from "@webpack"; -import { Alerts, moment, Parser, showToast, Timestamp } from "@webpack/common"; +import { Alerts, moment, Parser, Timestamp, useState } from "@webpack/common"; import { Auth, getToken } from "../auth"; import { Review, ReviewType } from "../entities"; import { blockUser, deleteReview, reportReview, unblockUser } from "../reviewDbApi"; import { settings } from "../settings"; -import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl } from "../utils"; +import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl, showToast } from "../utils"; import { openBlockModal } from "./BlockedUserModal"; import { BlockButton, DeleteButton, ReportButton } from "./MessageButton"; import ReviewBadge from "./ReviewBadge"; @@ -51,6 +51,8 @@ export default LazyComponent(() => { const dateFormat = new Intl.DateTimeFormat(); return function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) { + const [showAll, setShowAll] = useState(false); + function openModal() { openUserProfile(review.sender.discordID); } @@ -66,10 +68,9 @@ export default LazyComponent(() => { return showToast("You must be logged in to delete reviews."); } else { deleteReview(review.id).then(res => { - if (res.success) { + if (res) { refetch(); } - showToast(res.message); }); } } @@ -116,11 +117,11 @@ export default LazyComponent(() => { } return ( -
@@ -168,7 +169,9 @@ export default LazyComponent(() => { }
- {Parser.parseGuildEventDescription(review.comment)} + {(review.comment.length > 200 && !showAll) + ? [Parser.parseGuildEventDescription(review.comment.substring(0, 200)), "...",
, ( setShowAll(true)}>Read more)] + : Parser.parseGuildEventDescription(review.comment)}
{review.id !== 0 && ( diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index cfd5477db..64cea1815 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -18,13 +18,13 @@ import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react"; import { find, findByPropsLazy } from "@webpack"; -import { Forms, React, RelationshipStore, showToast, useRef, UserStore } from "@webpack/common"; +import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common"; import { Auth, authorize } from "../auth"; import { Review } from "../entities"; import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; import { settings } from "../settings"; -import { cl } from "../utils"; +import { cl, showToast } from "../utils"; import ReviewComponent from "./ReviewComponent"; @@ -168,7 +168,7 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { comment: res.value, }); - if (response?.success) { + if (response) { refetch(); const slateEditor = editorRef.current.ref.current.getSlateEditor(); @@ -180,8 +180,6 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { focus: Editor.end(slateEditor, []), } }); - } else if (response?.message) { - showToast(response.message); } // even tho we need to return this, it doesnt do anything diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index d8357faf2..50bb62184 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -25,7 +25,7 @@ import { OpenExternalIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin from "@utils/types"; -import { Alerts, Menu, Parser, showToast, useState } from "@webpack/common"; +import { Alerts, Menu, Parser, useState } from "@webpack/common"; import { Guild, User } from "discord-types/general"; import { Auth, initAuth, updateAuth } from "./auth"; @@ -34,6 +34,7 @@ import ReviewsView from "./components/ReviewsView"; import { NotificationType } from "./entities"; import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { settings } from "./settings"; +import { showToast } from "./utils"; const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => { children.push( diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts index 657e9c475..ec6d9ff3b 100644 --- a/src/plugins/reviewDB/reviewDbApi.ts +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -16,18 +16,18 @@ * along with this program. If not, see . */ -import { showToast, Toasts } from "@webpack/common"; +import { Toasts } from "@webpack/common"; import { Auth, authorize, getToken, updateAuth } from "./auth"; import { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from "./entities"; import { settings } from "./settings"; +import { showToast } from "./utils"; const API_URL = "https://manti.vendicated.dev/api/reviewdb"; export const REVIEWS_PER_PAGE = 50; export interface Response { - success: boolean, message: string; reviews: Review[]; updated: boolean; @@ -37,6 +37,16 @@ export interface Response { const WarningFlag = 0b00000010; +async function rdbRequest(path: string, options: RequestInit = {}) { + return fetch(API_URL + path, { + ...options, + headers: { + ...options.headers, + Authorization: await getToken() || "", + } + }); +} + export async function getReviews(id: string, offset = 0): Promise { let flags = 0; if (!settings.store.showWarning) flags |= WarningFlag; @@ -47,10 +57,9 @@ export async function getReviews(id: string, offset = 0): Promise { }); const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`); - const res = (req.status === 200) + const res = (req.ok) ? await req.json() as Response : { - success: false, message: req.status === 429 ? "You are sending requests too fast. Wait a few seconds and try again." : "An Error occured while fetching reviews. Please try again later.", reviews: [], updated: false, @@ -58,7 +67,7 @@ export async function getReviews(id: string, offset = 0): Promise { reviewCount: 0 }; - if (!res.success) { + if (!req.ok) { showToast(res.message, Toasts.Type.FAILURE); return { ...res, @@ -85,44 +94,46 @@ export async function getReviews(id: string, offset = 0): Promise { } export async function addReview(review: any): Promise { - review.token = await getToken(); - if (!review.token) { + const token = await getToken(); + if (!token) { showToast("Please authorize to add a review."); authorize(); return null; } - return fetch(API_URL + `/users/${review.userid}/reviews`, { + return await rdbRequest(`/users/${review.userid}/reviews`, { method: "PUT", body: JSON.stringify(review), headers: { "Content-Type": "application/json", } - }) - .then(r => r.json()) - .then(res => { - showToast(res.message); - return res ?? null; - }); + }).then(async r => { + const data = await r.json() as Response; + showToast(data.message); + return r.ok ? data : null; + }); } -export async function deleteReview(id: number): Promise { - return fetch(API_URL + `/users/${id}/reviews`, { +export async function deleteReview(id: number): Promise { + return await rdbRequest(`/users/${id}/reviews`, { method: "DELETE", headers: new Headers({ "Content-Type": "application/json", Accept: "application/json", }), body: JSON.stringify({ - token: await getToken(), reviewid: id }) - }).then(r => r.json()); + }).then(async r => { + const data = await r.json() as Response; + showToast(data.message); + return r.ok ? data : null; + }); } export async function reportReview(id: number) { - const res = await fetch(API_URL + "/reports", { + const res = await rdbRequest("/reports", { method: "PUT", headers: new Headers({ "Content-Type": "application/json", @@ -130,7 +141,6 @@ export async function reportReview(id: number) { }), body: JSON.stringify({ reviewid: id, - token: await getToken() }) }).then(r => r.json()) as Response; @@ -138,12 +148,11 @@ export async function reportReview(id: number) { } async function patchBlock(action: "block" | "unblock", userId: string) { - const res = await fetch(API_URL + "/blocks", { + const res = await rdbRequest("/blocks", { method: "PATCH", headers: new Headers({ "Content-Type": "application/json", Accept: "application/json", - Authorization: await getToken() || "" }), body: JSON.stringify({ action: action, @@ -169,11 +178,10 @@ export const blockUser = (userId: string) => patchBlock("block", userId); export const unblockUser = (userId: string) => patchBlock("unblock", userId); export async function fetchBlocks(): Promise { - const res = await fetch(API_URL + "/blocks", { + const res = await rdbRequest("/blocks", { method: "GET", headers: new Headers({ Accept: "application/json", - Authorization: await getToken() || "" }) }); @@ -182,17 +190,13 @@ export async function fetchBlocks(): Promise { } export function getCurrentUserInfo(token: string): Promise { - return fetch(API_URL + "/users", { - body: JSON.stringify({ token }), + return rdbRequest("/users", { method: "POST", }).then(r => r.json()); } export async function readNotification(id: number) { - return fetch(API_URL + `/notifications?id=${id}`, { - method: "PATCH", - headers: { - "Authorization": await getToken() || "", - }, + return rdbRequest(`/notifications?id=${id}`, { + method: "PATCH" }); } diff --git a/src/plugins/reviewDB/settings.tsx b/src/plugins/reviewDB/settings.tsx index efcb80588..79cf4174e 100644 --- a/src/plugins/reviewDB/settings.tsx +++ b/src/plugins/reviewDB/settings.tsx @@ -22,13 +22,14 @@ import { Button } from "@webpack/common"; import { authorize, getToken } from "./auth"; import { openBlockModal } from "./components/BlockedUserModal"; +import { cl } from "./utils"; export const settings = definePluginSettings({ authorize: { type: OptionType.COMPONENT, description: "Authorize with ReviewDB", component: () => ( - ) @@ -53,38 +54,40 @@ export const settings = definePluginSettings({ description: "Hide reviews from blocked users", default: true, }, - manageBlocks: { + buttons: { type: OptionType.COMPONENT, - description: "Manage Blocked Users", + description: "ReviewDB buttons", component: () => ( - - ) - }, - website: { - type: OptionType.COMPONENT, - description: "ReviewDB website", - component: () => ( - - VencordNative.native.openExternal(url); - }}> - ReviewDB website - - ) - }, - supportServer: { - type: OptionType.COMPONENT, - description: "ReviewDB Support Server", - component: () => ( - + + + + + + +
) } }).withPrivateSettings<{ diff --git a/src/plugins/reviewDB/style.css b/src/plugins/reviewDB/style.css index a812ecaf2..190b8f620 100644 --- a/src/plugins/reviewDB/style.css +++ b/src/plugins/reviewDB/style.css @@ -59,8 +59,14 @@ } .vc-rdb-review { - margin-top: 8px; - margin-bottom: 8px; + padding-top: 8px !important; + padding-bottom: 8px !important; + padding-right: 32px !important; +} + +.vc-rdb-review:hover { + background: var(--background-message-hover) !important; + border-radius: 8px; } .vc-rdb-review-comment img { @@ -91,6 +97,19 @@ gap: 0.75em; } +.vc-rdb-button-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +/* stylelint-disable-next-line media-feature-range-notation */ +@media (max-width: 600px) { + .vc-rdb-button-grid { + grid-template-columns: 1fr; + } +} + .vc-rdb-block-modal-row { display: flex; height: 2em; diff --git a/src/plugins/reviewDB/utils.tsx b/src/plugins/reviewDB/utils.tsx index eeaca1204..916adc45e 100644 --- a/src/plugins/reviewDB/utils.tsx +++ b/src/plugins/reviewDB/utils.tsx @@ -17,7 +17,7 @@ */ import { classNameFactory } from "@api/Styles"; -import { UserStore } from "@webpack/common"; +import { Toasts, UserStore } from "@webpack/common"; import { Auth } from "./auth"; import { Review, UserType } from "./entities"; @@ -41,3 +41,14 @@ export function canBlockReviewAuthor(profileId: string, review: Review) { export function canReportReview(review: Review) { return review.sender.discordID !== UserStore.getCurrentUser().id; } + +export function showToast(message: string, type = Toasts.Type.MESSAGE) { + Toasts.show({ + id: Toasts.genId(), + message, + type, + options: { + position: Toasts.Position.BOTTOM, // NOBODY LIKES TOASTS AT THE TOP + }, + }); +} From 4bb0db506682bf03b66366e70ae249979b95f695 Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Mon, 22 Jan 2024 01:21:10 +0100 Subject: [PATCH 036/356] fix anonymiseFileNames (#2125) --- src/plugins/anonymiseFileNames/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index 845aa756d..0382b65c2 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -81,7 +81,7 @@ export default definePlugin({ { find: ".Messages.ATTACHMENT_UTILITIES_SPOILER", replacement: { - match: /(?<=children:\[)(?=.{10,80}tooltip:\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/, + match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/, replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null," }, }, From 620c127b58e94ce4bf0b78d7e55fbce8e4f376e2 Mon Sep 17 00:00:00 2001 From: V Date: Thu, 25 Jan 2024 07:22:34 +0100 Subject: [PATCH 037/356] [skip ci] fix pnpm inject --- scripts/runInstaller.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/runInstaller.mjs b/scripts/runInstaller.mjs index b35039f8a..8b2510b2e 100644 --- a/scripts/runInstaller.mjs +++ b/scripts/runInstaller.mjs @@ -35,11 +35,11 @@ const ETAG_FILE = join(FILE_DIR, "etag.txt"); function getFilename() { switch (process.platform) { case "win32": - return "VencordInstaller.exe"; + return "VencordInstallerCli.exe"; case "darwin": return "VencordInstaller.MacOS.zip"; case "linux": - return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11"); + return "VencordInstallerCli-linux"; default: throw new Error("Unsupported platform: " + process.platform); } From cc885b5bb367add1c4e831ab1a4ab6145f6b0f5c Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 3 Feb 2024 02:11:59 +0100 Subject: [PATCH 038/356] remove lumap --- src/plugins/pictureInPicture/index.tsx | 2 +- src/utils/constants.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/pictureInPicture/index.tsx b/src/plugins/pictureInPicture/index.tsx index ba4aa8387..ca766affc 100644 --- a/src/plugins/pictureInPicture/index.tsx +++ b/src/plugins/pictureInPicture/index.tsx @@ -24,7 +24,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "PictureInPicture", description: "Adds picture in picture to videos (next to the Download button)", - authors: [Devs.Lumap], + authors: [Devs.Nobody], settings, patches: [ { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 899936128..55af93605 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -42,6 +42,10 @@ export interface Dev { * If you are fine with attribution but don't want the badge, add badge: false */ export const Devs = /* #__PURE__*/ Object.freeze({ + Nobody: { + name: "Nobody", + id: 0n, + }, Ven: { name: "Vendicated", id: 343383572805058560n @@ -359,10 +363,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "bb010g", id: 72791153467990016n, }, - Lumap: { - name: "lumap", - id: 635383782576357407n - }, Dolfies: { name: "Dolfies", id: 852892297661906993n, From 8938f4a3cf9a36aa490703651934acccbd07bdf9 Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Sun, 4 Feb 2024 01:50:51 +0100 Subject: [PATCH 039/356] fix moreUserTags (#2146) --- src/plugins/moreUserTags/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx index 9921bca8e..d1ad941b0 100644 --- a/src/plugins/moreUserTags/index.tsx +++ b/src/plugins/moreUserTags/index.tsx @@ -198,7 +198,7 @@ export default definePlugin({ replacement: [ // make the tag show the right text { - match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(\i\.\i\.Messages)\.BOT_TAG_BOT/, + match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.BOT_TAG_BOT/, replace: (_, origSwitch, variant, tags, displayedText, strings) => `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}` }, From bf977e0047141dd479b26f66cc848923310bcb6b Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 6 Feb 2024 16:29:47 +0100 Subject: [PATCH 040/356] Add chat bar button api ~ fixes buttons for russian users --- src/api/ChatButtons.tsx | 123 ++++++++++++++++++++ src/api/index.ts | 6 + src/plugins/_api/chatButtons.ts | 22 ++++ src/plugins/invisibleChat.desktop/index.tsx | 88 +++++--------- src/plugins/previewMessage/index.tsx | 74 +++++------- src/plugins/sendTimestamps/index.tsx | 101 +++++++--------- src/plugins/silentMessageToggle/index.tsx | 78 +++++-------- src/plugins/silentTyping/index.tsx | 66 ++++------- src/plugins/translate/TranslateIcon.tsx | 52 ++++----- src/plugins/translate/index.tsx | 22 +--- 10 files changed, 325 insertions(+), 307 deletions(-) create mode 100644 src/api/ChatButtons.tsx create mode 100644 src/plugins/_api/chatButtons.ts diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx new file mode 100644 index 000000000..0350965af --- /dev/null +++ b/src/api/ChatButtons.tsx @@ -0,0 +1,123 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Logger } from "@utils/Logger"; +import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; +import { Channel } from "discord-types/general"; +import { HTMLProps, MouseEventHandler, ReactNode } from "react"; + +export interface ChatBarProps { + channel: Channel; + disabled: boolean; + isEmpty: boolean; + type: { + analyticsName: string; + attachments: boolean; + autocomplete: { + addReactionShortcut: boolean, + forceChatLayer: boolean, + reactions: boolean; + }, + commands: { + enabled: boolean; + }, + drafts: { + type: number, + commandType: number, + autoSave: boolean; + }, + emojis: { + button: boolean; + }, + gifs: { + button: boolean, + allowSending: boolean; + }, + gifts: { + button: boolean; + }, + permissions: { + requireSendMessages: boolean; + }, + showThreadPromptOnReply: boolean, + stickers: { + button: boolean, + allowSending: boolean, + autoSuggest: boolean; + }, + users: { + allowMentioning: boolean; + }, + submit: { + button: boolean, + ignorePreference: boolean, + disableEnterToSubmit: boolean, + clearOnSubmit: boolean, + useDisabledStylesOnSubmit: boolean; + }, + uploadLongMessages: boolean, + upsellLongMessages: { + iconOnly: boolean; + }, + showCharacterCount: boolean, + sedReplace: boolean; + }; +} + +export type ChatBarButton = (props: ChatBarProps, isMainChat: boolean) => ReactNode; + +const buttonFactories = new Map(); +const logger = new Logger("ChatButtons"); + +export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) { + if (props.type.analyticsName !== "normal") return; + + for (const [key, makeButton] of buttonFactories) { + try { + const res = makeButton(props, props.type.analyticsName === "normal"); + if (res) buttons.push(res); + } catch (e) { + logger.error(`Failed to render button ${key}`, e); + } + } +} + +export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button); +export const removeChatBarButton = (id: string) => buttonFactories.delete(id); + +export interface ChatBarButtonProps { + children: ReactNode; + tooltip: string; + onClick: MouseEventHandler; + onContextMenu?: MouseEventHandler; + buttonProps?: Omit, "size" | "onClick" | "onContextMenu">; +} +export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { + return ( + + {({ onMouseEnter, onMouseLeave }) => ( +
+ +
+ )} +
+ ); +}, { noop: true }); diff --git a/src/api/index.ts b/src/api/index.ts index 08f238104..5dca63105 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -17,6 +17,7 @@ */ import * as $Badges from "./Badges"; +import * as $ChatButtons from "./ChatButtons"; import * as $Commands from "./Commands"; import * as $ContextMenu from "./ContextMenu"; import * as $DataStore from "./DataStore"; @@ -104,3 +105,8 @@ export const Notifications = $Notifications; * An api allowing you to patch and add/remove items to/from context menus */ export const ContextMenu = $ContextMenu; + +/** + * An API allowing you to add buttons to the chat input + */ +export const ChatButtons = $ChatButtons; diff --git a/src/plugins/_api/chatButtons.ts b/src/plugins/_api/chatButtons.ts new file mode 100644 index 000000000..ca85964c0 --- /dev/null +++ b/src/plugins/_api/chatButtons.ts @@ -0,0 +1,22 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "ChatInputButtonAPI", + description: "API to add buttons to the chat input", + authors: [Devs.Ven], + + patches: [{ + find: 'location:"ChannelTextAreaButtons"', + replacement: { + match: /if\(!\i\.isMobile\)\{(?=.+?&&(\i)\.push\(.{0,50}"gift")/, + replace: "$&Vencord.Api.ChatButtons._injectButtons($1,arguments[0]);" + } + }] +}); diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx index c80c4ce54..3184e025d 100644 --- a/src/plugins/invisibleChat.desktop/index.tsx +++ b/src/plugins/invisibleChat.desktop/index.tsx @@ -16,13 +16,14 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getStegCloak } from "@utils/dependencies"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; import { buildDecModal } from "./components/DecryptionModal"; @@ -64,54 +65,32 @@ function Indicator() { } -function ChatBarIcon(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - if (chatBoxProps.type.analyticsName !== "normal") return null; +const ChatBarIcon: ChatBarButton = (_, isMainChat) => { + if (!isMainChat) return null; return ( - - {({ onMouseEnter, onMouseLeave }) => ( - // size="" = Button.Sizes.NONE - /* - many themes set "> button" to display: none, as the gift button is - the only directly descending button (all the other elements are divs.) - Thus, wrap in a div here to avoid getting hidden by that. - flex is for some reason necessary as otherwise the button goes flying off - */ -
- -
- ) - } -
+ buildEncModal()} + + buttonProps={{ + "aria-haspopup": "dialog", + style: { padding: "0 2px", scale: "0.9" } + }} + > + + + + ); -} +}; const settings = definePluginSettings({ savedPasswords: { @@ -125,7 +104,7 @@ export default definePlugin({ name: "InvisibleChat", description: "Encrypt your Messages in a non-suspicious way!", authors: [Devs.SammCheese], - dependencies: ["MessagePopoverAPI"], + dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"], patches: [ { // Indicator @@ -135,13 +114,6 @@ export default definePlugin({ replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&" } }, - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, ], EMBED_API_URL: "https://embed.sammcheese.net", @@ -154,7 +126,7 @@ export default definePlugin({ const { default: StegCloak } = await getStegCloak(); steggo = new StegCloak(true, false); - addButton("invDecrypt", message => { + addButton("InvisibleChat", message => { return this.INV_REGEX.test(message?.content) ? { label: "Decrypt Message", @@ -170,10 +142,13 @@ export default definePlugin({ } : null; }); + + addChatBarButton("InvisibleChat", ChatBarIcon); }, stop() { - removeButton("invDecrypt"); + removeButton("InvisibleChat"); + removeButton("InvisibleChat"); }, // Gets the Embed of a Link @@ -216,7 +191,6 @@ export default definePlugin({ }); }, - chatBarIcon: ErrorBoundary.wrap(ChatBarIcon, { noop: true }), popOverIcon: () => , indicator: ErrorBoundary.wrap(Indicator, { noop: true }) }); diff --git a/src/plugins/previewMessage/index.tsx b/src/plugins/previewMessage/index.tsx index f2634ae6b..1d8b769d5 100644 --- a/src/plugins/previewMessage/index.tsx +++ b/src/plugins/previewMessage/index.tsx @@ -16,22 +16,14 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { generateId, sendBotMessage } from "@api/Commands"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; +import { DraftStore, DraftType, SelectedChannelStore, UserStore, useStateFromStores } from "@webpack/common"; import { MessageAttachment } from "discord-types/general"; -interface Props { - type: { - analyticsName: string; - isEmpty: boolean; - attachments: boolean; - }; -} - const UploadStore = findByPropsLazy("getUploads"); const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); @@ -81,13 +73,13 @@ const getAttachments = async (channelId: string) => ); -export function PreviewButton(chatBoxProps: Props) { - const { isEmpty, attachments } = chatBoxProps.type; +const PreviewButton: ChatBarButton = (props, isMainChat) => { + const { isEmpty, type: { attachments } } = props; const channelId = SelectedChannelStore.getChannelId(); const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat) return null; const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0; const hasContent = !isEmpty && draft?.length > 0; @@ -95,47 +87,33 @@ export function PreviewButton(chatBoxProps: Props) { if (!hasContent && !hasAttachments) return null; return ( - - {tooltipProps => ( - - )} - + + sendBotMessage( + channelId, + { + content: getDraft(channelId), + author: UserStore.getCurrentUser(), + attachments: hasAttachments ? await getAttachments(channelId) : undefined, + } + )} + buttonProps={{ + style: { padding: "0 2px", height: "100%" } + }} + > + + ); -} +}; export default definePlugin({ name: "PreviewMessage", description: "Lets you preview your message before sending it.", authors: [Devs.Aria], - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], + dependencies: ["ChatInputButtonAPI"], - chatBarIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }), + start: () => addChatBarButton("previewMessage", PreviewButton), + stop: () => removeChatBarButton("previewMessage"), }); diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx index 6d488add6..bd888a82b 100644 --- a/src/plugins/sendTimestamps/index.tsx +++ b/src/plugins/sendTimestamps/index.tsx @@ -18,6 +18,7 @@ import "./styles.css"; +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; @@ -26,7 +27,7 @@ import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, Forms, Parser, Select, Tooltip, useMemo, useState } from "@webpack/common"; +import { Button, Forms, Parser, Select, useMemo, useState } from "@webpack/common"; const settings = definePluginSettings({ replaceMessageContents: { @@ -122,25 +123,51 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi ); } +const ChatBarIcon: ChatBarButton = (_, isMainChat) => { + if (!isMainChat) return null; + + return ( + { + const key = openModal(props => ( + closeModal(key)} + /> + )); + }} + buttonProps={{ + "aria-haspopup": "dialog", + className: cl("button") + }} + > + + + ); +}; + export default definePlugin({ name: "SendTimestamps", description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!", authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11], - dependencies: ["MessageEventsAPI"], + dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], - settings: settings, - - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], + settings, start() { + addChatBarButton("SendTimestamps", ChatBarIcon); this.listener = addPreSendListener((_, msg) => { if (settings.store.replaceMessageContents) { msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); @@ -149,56 +176,10 @@ export default definePlugin({ }, stop() { + removeChatBarButton("SendTimestamps"); removePreSendListener(this.listener); }, - chatBarIcon(chatBoxProps: { type: { analyticsName: string; }; }) { - if (chatBoxProps.type.analyticsName !== "normal") return null; - - return ( - - {({ onMouseEnter, onMouseLeave }) => ( -
- -
- ) - } -
- ); - }, - settingsAboutComponent() { const samples = [ "12:00", diff --git a/src/plugins/silentMessageToggle/index.tsx b/src/plugins/silentMessageToggle/index.tsx index b7b33826d..6c7d179be 100644 --- a/src/plugins/silentMessageToggle/index.tsx +++ b/src/plugins/silentMessageToggle/index.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, React, Tooltip } from "@webpack/common"; +import { React, useEffect, useState } from "@webpack/common"; let lastState = false; @@ -41,19 +41,15 @@ const settings = definePluginSettings({ } }); -function SilentMessageToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const [enabled, setEnabled] = React.useState(lastState); +const SilentMessageToggle: ChatBarButton = (_, isMainChat) => { + const [enabled, setEnabled] = useState(lastState); function setEnabledValue(value: boolean) { if (settings.store.persistState) lastState = value; setEnabled(value); } - React.useEffect(() => { + useEffect(() => { const listener: SendListener = (_, message) => { if (enabled) { if (settings.store.autoDisable) setEnabledValue(false); @@ -65,55 +61,37 @@ function SilentMessageToggle(chatBoxProps: { return () => void removePreSendListener(listener); }, [enabled]); - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat) return null; return ( - - {tooltipProps => ( -
- -
- )} -
+ setEnabledValue(!enabled)} + buttonProps={{ + style: { padding: "0 6px" } + }} + > + + + {!enabled && <> + + + + + + } + + ); -} +}; export default definePlugin({ name: "SilentMessageToggle", authors: [Devs.Nuckyz, Devs.CatNoir], description: "Adds a button to the chat bar to toggle sending a silent message.", - dependencies: ["MessageEventsAPI"], - + dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], settings, - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], - chatBarIcon: ErrorBoundary.wrap(SilentMessageToggle, { noop: true }), + start: () => addChatBarButton("SilentMessageToggle", SilentMessageToggle), + stop: () => removeChatBarButton("SilentMessageToggle") }); diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index dae7ad4c9..1d336b477 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, React, Tooltip } from "@webpack/common"; +import { FluxDispatcher, React } from "@webpack/common"; const settings = definePluginSettings({ showIcon: { @@ -37,45 +37,35 @@ const settings = definePluginSettings({ } }); -function SilentTypingToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const { isEnabled } = settings.use(["isEnabled"]); +const SilentTypingToggle: ChatBarButton = (_, isMainChat) => { + const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]); const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat || !showIcon) return null; return ( - - {(tooltipProps: any) => ( -
- -
- )} -
+ + + + {isEnabled && } + + ); -} +}; export default definePlugin({ name: "SilentTyping", authors: [Devs.Ven, Devs.Rini], description: "Hide that you are typing", + dependencies: ["CommandsAPI", "ChatInputButtonAPI"], + settings, + patches: [ { find: '.dispatch({type:"TYPING_START_LOCAL"', @@ -84,17 +74,8 @@ export default definePlugin({ replace: "startTyping:$self.startTyping,stop" } }, - { - find: "ChannelTextAreaButtons", - predicate: () => settings.store.showIcon, - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, ], - dependencies: ["CommandsAPI"], - settings, + commands: [{ name: "silenttype", description: "Toggle whether you're hiding that you're typing or not.", @@ -120,5 +101,6 @@ export default definePlugin({ FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); }, - chatBarIcon: ErrorBoundary.wrap(SilentTypingToggle, { noop: true }), + start: () => addChatBarButton("SilentTyping", SilentTypingToggle), + stop: () => removeChatBarButton("SilentTyping"), }); diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx index 649589435..a7d789279 100644 --- a/src/plugins/translate/TranslateIcon.tsx +++ b/src/plugins/translate/TranslateIcon.tsx @@ -16,9 +16,9 @@ * along with this program. If not, see . */ +import { ChatBarButton } from "@api/ChatButtons"; import { classes } from "@utils/misc"; import { openModal } from "@utils/modal"; -import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; import { settings } from "./settings"; import { TranslateModal } from "./TranslateModal"; @@ -37,42 +37,30 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?: ); } -export function TranslateChatBarIcon({ slateProps }: { slateProps: { type: { analyticsName: string; }; }; }) { +export const TranslateChatBarIcon: ChatBarButton = (props, isMainChat) => { const { autoTranslate } = settings.use(["autoTranslate"]); - if (slateProps.type.analyticsName !== "normal") - return null; + if (!isMainChat) return null; const toggle = () => settings.store.autoTranslate = !autoTranslate; return ( - - {({ onMouseEnter, onMouseLeave }) => ( -
- -
- )} -
+ openModal(props => ( + + )); + }} + onContextMenu={() => toggle()} + buttonProps={{ + "aria-haspopup": "dialog", + style: { padding: "0 4px" } + }} + > + + ); -} +}; diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index 3b067c634..702e60cf7 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -18,11 +18,11 @@ import "./styles.css"; +import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { addButton, removeButton } from "@api/MessagePopover"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { ChannelStore, Menu } from "@webpack/common"; @@ -55,25 +55,16 @@ export default definePlugin({ name: "Translate", description: "Translate messages with Google Translate", authors: [Devs.Ven], - dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI"], + dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], settings, // not used, just here in case some other plugin wants it or w/e translate, - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], - start() { addAccessory("vc-translation", props => ); addContextMenuPatch("message", messageCtxPatch); + addChatBarButton("vc-translate", TranslateChatBarIcon); addButton("vc-translate", message => { if (!message.content) return null; @@ -101,13 +92,8 @@ export default definePlugin({ stop() { removePreSendListener(this.preSend); removeContextMenuPatch("message", messageCtxPatch); + removeChatBarButton("vc-translate"); removeButton("vc-translate"); removeAccessory("vc-translation"); }, - - chatBarIcon: (slateProps: any) => ( - - - - ) }); From 2c198e547ce2424ada0c8e2c067b2d2c7b435c7a Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 6 Feb 2024 16:50:21 +0100 Subject: [PATCH 041/356] Fix PreviewMessage icon being offcentre --- src/api/ChatButton.css | 4 ++++ src/api/ChatButtons.tsx | 8 +++++++- src/plugins/invisibleChat.desktop/index.tsx | 6 +++--- src/plugins/previewMessage/index.tsx | 10 ++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 src/api/ChatButton.css diff --git a/src/api/ChatButton.css b/src/api/ChatButton.css new file mode 100644 index 000000000..30869a846 --- /dev/null +++ b/src/api/ChatButton.css @@ -0,0 +1,4 @@ +.vc-chatbar-button { + display: flex; + align-items: center; +} diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx index 0350965af..c995033b1 100644 --- a/src/api/ChatButtons.tsx +++ b/src/api/ChatButtons.tsx @@ -4,12 +4,18 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import "./ChatButton.css"; + import ErrorBoundary from "@components/ErrorBoundary"; import { Logger } from "@utils/Logger"; +import { waitFor } from "@webpack"; import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; import { Channel } from "discord-types/general"; import { HTMLProps, MouseEventHandler, ReactNode } from "react"; +let CssClasses: { buttonContainer: string; }; +waitFor(["buttonContainer", "channelTextArea"], m => CssClasses = m); + export interface ChatBarProps { channel: Channel; disabled: boolean; @@ -100,7 +106,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { return ( {({ onMouseEnter, onMouseLeave }) => ( -
+
- {viewAllowedUsersAndRoles && } + {defaultAllowedUsersAndRolesDropdownState && }
From d3bbd2c02ebcab88f0c6a7b19e7687eca6068e1c Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:00:15 -0300 Subject: [PATCH 058/356] FakeNitro: option to use hyperlinks or not --- src/plugins/fakeNitro/index.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/plugins/fakeNitro/index.ts b/src/plugins/fakeNitro/index.ts index 2caecc45b..ed3ec59dd 100644 --- a/src/plugins/fakeNitro/index.ts +++ b/src/plugins/fakeNitro/index.ts @@ -157,6 +157,11 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true, restartNeeded: true + }, + useHyperLinks: { + description: "Whether to use hyperlinks when sending fake emojis and stickers", + type: OptionType.BOOLEAN, + default: true } }); @@ -708,7 +713,7 @@ export default definePlugin({ }, getStickerLink(stickerId: string) { - return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`; + return `https://media.discordapp.net/stickers/${stickerId}.png?size=${settings.store.stickerSize}`; }, async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) { @@ -813,7 +818,7 @@ export default definePlugin({ const url = new URL(link); url.searchParams.set("name", sticker.name); - messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}[${sticker.name}](${url})`; + messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${sticker.name}](${url})` : url}`; extra.stickers!.length = 0; } } @@ -829,11 +834,11 @@ export default definePlugin({ const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; const url = new URL(emoji.url); - url.searchParams.set("size", settings.store.emojiSize.toString()); + url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("name", emoji.name); messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}[:${emoji.name}:](${url})${getWordBoundary(origStr, offset + match.length)}`; + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[:${emoji.name}:](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`; }); } } @@ -856,10 +861,10 @@ export default definePlugin({ if (emoji.guildId === guildId && !emoji.animated) return emojiStr; const url = new URL(emoji.url); - url.searchParams.set("size", settings.store.emojiSize.toString()); + url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("name", emoji.name); - return `${getWordBoundary(origStr, offset - 1)}[:${emoji.name}:](${url})${getWordBoundary(origStr, offset + emojiStr.length)}`; + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[:${emoji.name}:](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; }); }); }, From cc0d9a90bc503f6c2055e543eb1414b92127e8b3 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:27:34 -0300 Subject: [PATCH 059/356] Fix UserVoiceShow patch --- src/plugins/userVoiceShow/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx index c95307cc4..935ff1c5d 100644 --- a/src/plugins/userVoiceShow/index.tsx +++ b/src/plugins/userVoiceShow/index.tsx @@ -96,7 +96,7 @@ export default definePlugin({ patches: [ // above message box { - find: ".lastEditedByContainer", + find: ".popularApplicationCommandIds,", replacement: { match: /\(0,\i\.jsx\)\(\i\.\i,{user:\i,setNote/, replace: "$self.patchPopout(arguments[0]),$&", From 38beb93e5f2ea7f7ea0c6021ddaf0ecb8f600406 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 10 Feb 2024 13:42:31 -0300 Subject: [PATCH 060/356] Fix CrashHandler failing to recover and showing gray screen instead --- src/plugins/crashHandler/index.ts | 96 ++++++++++++++----------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts index 9d38b7d17..b73641f57 100644 --- a/src/plugins/crashHandler/index.ts +++ b/src/plugins/crashHandler/index.ts @@ -25,7 +25,6 @@ import definePlugin, { OptionType } from "@utils/types"; import { maybePromptToUpdate } from "@utils/updater"; import { filters, findBulk, proxyLazyWebpack } from "@webpack"; import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common"; -import type { ReactElement } from "react"; const CrashHandlerLogger = new Logger("CrashHandler"); const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => { @@ -57,13 +56,12 @@ const settings = definePluginSettings({ } }); -let crashCount: number = 0; -let lastCrashTimestamp: number = 0; -let shouldAttemptNextHandle = false; +let hasCrashedOnce = false; +let shouldAttemptRecover = true; export default definePlugin({ name: "CrashHandler", - description: "Utility plugin for handling and possibly recovering from Crashes without a restart", + description: "Utility plugin for handling and possibly recovering from crashes without a restart", authors: [Devs.Nuckyz], enabledByDefault: true, @@ -74,60 +72,55 @@ export default definePlugin({ find: ".Messages.ERRORS_UNEXPECTED_CRASH", replacement: { match: /(?=this\.setState\()/, - replace: "$self.handleCrash(this)||" + replace: "$self.handleCrash(this);" } } ], - handleCrash(_this: ReactElement & { forceUpdate: () => void; }) { - if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true; + handleCrash(_this: any) { + // 1 ms timeout to avoid react breaking when re-rendering + setTimeout(() => { + if (!shouldAttemptRecover) { + try { + showNotification({ + color: "#eed202", + title: "Discord has crashed!", + body: "Awn :( Discord has crashed two times rapidly, not attempting to recover.", + noPersist: true, + }); + } catch { } - shouldAttemptNextHandle = false; - - if (++crashCount > 5) { - try { - showNotification({ - color: "#eed202", - title: "Discord has crashed!", - body: "Awn :( Discord has crashed more than five times, not attempting to recover.", - noPersist: true, - }); - } catch { } - - lastCrashTimestamp = Date.now(); - return false; - } - - setTimeout(() => crashCount--, 60_000); - - try { - if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true); - - if (settings.store.attemptToPreventCrashes) { - this.handlePreventCrash(_this); - return true; + return; } - return false; - } catch (err) { - CrashHandlerLogger.error("Failed to handle crash", err); - return false; - } finally { - lastCrashTimestamp = Date.now(); - } + shouldAttemptRecover = false; + // This is enough to avoid a crash loop + setTimeout(() => shouldAttemptRecover = true, 500); + + try { + if (!hasCrashedOnce) { + hasCrashedOnce = true; + maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true); + } + + if (settings.store.attemptToPreventCrashes) { + this.handlePreventCrash(_this); + } + } catch (err) { + CrashHandlerLogger.error("Failed to handle crash", err); + } + }, 1); }, - handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) { - if (Date.now() - lastCrashTimestamp >= 1_000) { - try { - showNotification({ - color: "#eed202", - title: "Discord has crashed!", - body: "Attempting to recover...", - noPersist: true, - }); - } catch { } - } + handlePreventCrash(_this: any) { + try { + showNotification({ + color: "#eed202", + title: "Discord has crashed!", + body: "Attempting to recover...", + noPersist: true, + }); + } catch { } try { const channelId = SelectedChannelStore.getChannelId(); @@ -177,8 +170,7 @@ export default definePlugin({ } try { - shouldAttemptNextHandle = true; - _this.forceUpdate(); + _this.setState({ error: null, info: null }); } catch (err) { CrashHandlerLogger.debug("Failed to update crash handler component.", err); } From 8b6a40311bab1c89a11d2384508b997e99c7b59b Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 13 Feb 2024 09:04:28 +0100 Subject: [PATCH 061/356] Bump to v1.6.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 076b2999d..08078d3d5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.7", + "version": "1.6.8", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 0c9d2a6a21a42111465c9cecde729c878ffd1c08 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 13 Feb 2024 09:03:25 +0100 Subject: [PATCH 062/356] Fix plugins using the Timestamp component --- src/api/Notifications/notificationLog.tsx | 4 ++-- src/plugins/reviewDB/components/ReviewComponent.tsx | 4 ++-- src/plugins/serverProfile/GuildProfileModal.tsx | 4 ++-- .../components/HiddenChannelLockScreen.tsx | 6 +++--- src/webpack/common/types/components.d.ts | 3 +-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/api/Notifications/notificationLog.tsx b/src/api/Notifications/notificationLog.tsx index 9535fb62c..6f79ef70a 100644 --- a/src/api/Notifications/notificationLog.tsx +++ b/src/api/Notifications/notificationLog.tsx @@ -21,7 +21,7 @@ import { Settings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; -import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; +import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; import { nanoid } from "nanoid"; import type { DispatchWithoutAction } from "react"; @@ -129,7 +129,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) { richBody={
{data.body} - +
} /> diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx index 977745a25..20b298ccb 100644 --- a/src/plugins/reviewDB/components/ReviewComponent.tsx +++ b/src/plugins/reviewDB/components/ReviewComponent.tsx @@ -20,7 +20,7 @@ import { openUserProfile } from "@utils/discord"; import { classes } from "@utils/misc"; import { LazyComponent } from "@utils/react"; import { filters, findBulk } from "@webpack"; -import { Alerts, moment, Parser, Timestamp, useState } from "@webpack/common"; +import { Alerts, Parser, Timestamp, useState } from "@webpack/common"; import { Auth, getToken } from "../auth"; import { Review, ReviewType } from "../entities"; @@ -163,7 +163,7 @@ export default LazyComponent(() => { { !settings.store.hideTimestamps && review.type !== ReviewType.System && ( - + {dateFormat.format(review.timestamp * 1000)} ) } diff --git a/src/plugins/serverProfile/GuildProfileModal.tsx b/src/plugins/serverProfile/GuildProfileModal.tsx index 97b40b764..be1f2cd54 100644 --- a/src/plugins/serverProfile/GuildProfileModal.tsx +++ b/src/plugins/serverProfile/GuildProfileModal.tsx @@ -12,7 +12,7 @@ import { classes } from "@utils/misc"; import { ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import { findByPropsLazy, findExportedComponentLazy } from "@webpack"; -import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common"; +import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common"; import { Guild, User } from "discord-types/general"; const IconUtils = findByPropsLazy("getGuildBannerURL"); @@ -50,7 +50,7 @@ const fetched = { function renderTimestamp(timestamp: number) { return ( - + ); } diff --git a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx index 7904cd10b..a8f5735e7 100644 --- a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx +++ b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx @@ -20,7 +20,7 @@ import { Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { formatDuration } from "@utils/text"; import { findByPropsLazy, findComponentByCodeLazy, findComponentLazy } from "@webpack"; -import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common"; +import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip, useEffect, useState } from "@webpack/common"; import type { Channel } from "discord-types/general"; import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "../../permissionsViewer/components/RolesAndUsersPermissions"; @@ -216,12 +216,12 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { {lastMessageId && Last {channel.isForumChannel() ? "post" : "message"} created: - + } {lastPinTimestamp && - Last message pin: + Last message pin: } {(rateLimitPerUser ?? 0) > 0 && Slowmode: {formatDuration(rateLimitPerUser!, "seconds")} diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index b9bc434c6..72a8a69b4 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import type { Moment } from "moment"; import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; @@ -154,7 +153,7 @@ export type Switch = ComponentType>; export type Timestamp = ComponentType Date: Wed, 14 Feb 2024 15:00:29 -0300 Subject: [PATCH 063/356] Attempt to fix CrashHandler odd issues --- src/plugins/crashHandler/index.ts | 52 ++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts index b73641f57..f8c76d7f7 100644 --- a/src/plugins/crashHandler/index.ts +++ b/src/plugins/crashHandler/index.ts @@ -57,6 +57,7 @@ const settings = definePluginSettings({ }); let hasCrashedOnce = false; +let isRecovering = false; let shouldAttemptRecover = true; export default definePlugin({ @@ -71,38 +72,49 @@ export default definePlugin({ { find: ".Messages.ERRORS_UNEXPECTED_CRASH", replacement: { - match: /(?=this\.setState\()/, - replace: "$self.handleCrash(this);" + match: /this\.setState\((.+?)\)/, + replace: "$self.handleCrash(this,$1);" } } ], - handleCrash(_this: any) { + handleCrash(_this: any, errorState: any) { + _this.setState(errorState); + + // Already recovering, prevent error which happens more than once too fast to trigger another recover + if (isRecovering) return; + isRecovering = true; + // 1 ms timeout to avoid react breaking when re-rendering setTimeout(() => { - if (!shouldAttemptRecover) { - try { - showNotification({ - color: "#eed202", - title: "Discord has crashed!", - body: "Awn :( Discord has crashed two times rapidly, not attempting to recover.", - noPersist: true, - }); - } catch { } + try { + // Prevent a crash loop with an error that could not be handled + if (!shouldAttemptRecover) { + try { + showNotification({ + color: "#eed202", + title: "Discord has crashed!", + body: "Awn :( Discord has crashed two times rapidly, not attempting to recover.", + noPersist: true + }); + } catch { } - return; - } + return; + } - shouldAttemptRecover = false; - // This is enough to avoid a crash loop - setTimeout(() => shouldAttemptRecover = true, 500); + shouldAttemptRecover = false; + // This is enough to avoid a crash loop + setTimeout(() => shouldAttemptRecover = true, 500); + } catch { } try { if (!hasCrashedOnce) { hasCrashedOnce = true; maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true); } + } catch { } + try { if (settings.store.attemptToPreventCrashes) { this.handlePreventCrash(_this); } @@ -118,7 +130,7 @@ export default definePlugin({ color: "#eed202", title: "Discord has crashed!", body: "Attempting to recover...", - noPersist: true, + noPersist: true }); } catch { } @@ -169,6 +181,10 @@ export default definePlugin({ } } + + // Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens + setImmediate(() => isRecovering = false); + try { _this.setState({ error: null, info: null }); } catch (err) { From 46ee193cd0d27d6b9f861858621487ec9c78b397 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:07:51 -0300 Subject: [PATCH 064/356] Fix MessageLogger edit Timestamp component --- src/plugins/messageLogger/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index ef0aa03bb..ef986bf87 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -26,7 +26,7 @@ import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher, i18n, Menu, moment, Parser, Timestamp, UserStore } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, i18n, Menu, Parser, Timestamp, UserStore } from "@webpack/common"; import overlayStyle from "./deleteStyleOverlay.css?managed"; import textStyle from "./deleteStyleText.css?managed"; @@ -122,7 +122,7 @@ export default definePlugin({ makeEdit(newMessage: any, oldMessage: any): any { return { - timestamp: moment?.call(newMessage.edited_timestamp), + timestamp: new Date(newMessage.edited_timestamp), content: oldMessage.content }; }, From 93b2095d71a0ac9d68d9f9859dd8b59687885be0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:16:31 -0300 Subject: [PATCH 065/356] Fix FakeNitro hyperlinks not working sometimes It is caused by emoji names with conflict with default emojis, but have a letter capitalized --- src/plugins/fakeNitro/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/fakeNitro/index.ts b/src/plugins/fakeNitro/index.ts index ed3ec59dd..560cae381 100644 --- a/src/plugins/fakeNitro/index.ts +++ b/src/plugins/fakeNitro/index.ts @@ -838,7 +838,7 @@ export default definePlugin({ url.searchParams.set("name", emoji.name); messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[:${emoji.name}:](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`; + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`; }); } } @@ -864,7 +864,7 @@ export default definePlugin({ url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("name", emoji.name); - return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[:${emoji.name}:](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; }); }); }, From f1bdf385ebbd5418cf6798e599e2c1d4240ecadb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:25:10 -0300 Subject: [PATCH 066/356] Bump to 1.6.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08078d3d5..5ffac7ff5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.8", + "version": "1.6.9", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 89367e3b2a9e02767f8aca56e3ac7c34ac0d285e Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 15 Feb 2024 09:20:27 +0100 Subject: [PATCH 067/356] WebContextMenus: fix copying images it broke because of the new url expiry parameters --- src/plugins/webContextMenus.web/index.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index 5f6beca2c..faa240783 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -47,18 +47,23 @@ const settings = definePluginSettings({ }); const MEDIA_PROXY_URL = "https://media.discordapp.net"; -const CDN_URL = "https://cdn.discordapp.com"; +const CDN_URL = "cdn.discordapp.com"; -function fixImageUrl(urlString: string, explodeWebp: boolean) { +function fixImageUrl(urlString: string) { const url = new URL(urlString); - if (url.origin === CDN_URL) return urlString; - if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname; + if (url.host === CDN_URL) return urlString; url.searchParams.delete("width"); url.searchParams.delete("height"); - url.searchParams.set("quality", "lossless"); - if (explodeWebp && url.searchParams.get("format") === "webp") - url.searchParams.set("format", "png"); + + if (url.origin === MEDIA_PROXY_URL) { + url.host = CDN_URL; + url.searchParams.delete("size"); + url.searchParams.delete("quality"); + url.searchParams.delete("format"); + } else { + url.searchParams.set("quality", "lossless"); + } return url.toString(); } @@ -199,7 +204,7 @@ export default definePlugin({ ], async copyImage(url: string) { - url = fixImageUrl(url, true); + url = fixImageUrl(url); let imageData = await fetch(url).then(r => r.blob()); if (imageData.type !== "image/png") { @@ -231,7 +236,7 @@ export default definePlugin({ }, async saveImage(url: string) { - url = fixImageUrl(url, false); + url = fixImageUrl(url); const data = await fetchImage(url); if (!data) return; From bc0a55053d709533143f66a2bc4b6914326e907d Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 15 Feb 2024 10:12:33 +0100 Subject: [PATCH 068/356] MessageLinkEmbeds: fix erroring on some invalid message links --- src/plugins/messageLinkEmbeds/index.tsx | 35 ++++++++++--------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx index 762829992..56facf2e7 100644 --- a/src/plugins/messageLinkEmbeds/index.tsx +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -31,13 +31,14 @@ import { GuildStore, MessageStore, Parser, + PermissionsBits, PermissionStore, RestAPI, Text, TextAndImagesSettingsStores, UserStore } from "@webpack/common"; -import { Channel, Guild, Message } from "discord-types/general"; +import { Channel, Message } from "discord-types/general"; const messageCache = new Map id && idList.includes(id)); + const isListed = [linkedChannel.guild_id, channelID, message.author.id].some(id => id && idList.includes(id)); if (listMode === "blacklist" && isListed) continue; if (listMode === "whitelist" && !isListed) continue; @@ -265,8 +265,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) { const messageProps: MessageEmbedProps = { message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]), - channel: linkedChannel, - guildID + channel: linkedChannel }; const type = settings.store.automodEmbeds; @@ -280,10 +279,8 @@ function MessageEmbedAccessory({ message }: { message: Message; }) { return accessories.length ? <>{accessories} : null; } -function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null { - const isDM = guildID === "@me"; - - const guild = !isDM && GuildStore.getGuild(channel.guild_id); +function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps): JSX.Element | null { + const guild = !channel.isDM() && GuildStore.getGuild(channel.guild_id); const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]); @@ -293,11 +290,8 @@ function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbe color: "var(--background-secondary)", author: { name: - {isDM ? "Direct Message - " : (guild as Guild).name + " - "} - {isDM - ? Parser.parse(`<@${dmReceiver.id}>`) - : Parser.parse(`<#${channel.id}>`) - } + {channel.isDM() && Direct Message - } + {Parser.parse(channel.isDM() ? `<@${dmReceiver.id}>` : `<#${channel.id}>`)} , iconProxyURL: guild ? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png` @@ -318,9 +312,8 @@ function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbe } function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { - const { message, channel, guildID } = props; + const { message, channel } = props; const compact = TextAndImagesSettingsStores.MessageDisplayCompact.useSetting(); - const isDM = guildID === "@me"; const images = getImages(message); const { parse } = Parser; @@ -328,11 +321,11 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { channel={channel} childrenAccessories={ - {isDM + {channel.isDM() ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) : parse(`<#${channel.id}>`) } - {isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name} + {channel.isDM() && - Direct Message} } compact={compact} From a501da692f5c6584ae202fade81794e9fee00ef4 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 15 Feb 2024 10:36:01 +0100 Subject: [PATCH 069/356] MessageLinkEmbeds: fix group dm support, improve ui --- src/plugins/messageLinkEmbeds/index.tsx | 79 +++++++++++-------- src/plugins/mutualGroupDMs/index.tsx | 5 +- .../serverProfile/GuildProfileModal.tsx | 12 +-- src/plugins/viewIcons/index.tsx | 21 ++--- src/webpack/common/types/utils.d.ts | 45 +++++++++++ src/webpack/common/utils.ts | 2 + 6 files changed, 107 insertions(+), 57 deletions(-) diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx index 56facf2e7..50c09ec9f 100644 --- a/src/plugins/messageLinkEmbeds/index.tsx +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -29,6 +29,7 @@ import { ChannelStore, FluxDispatcher, GuildStore, + IconUtils, MessageStore, Parser, PermissionsBits, @@ -50,6 +51,7 @@ const AutoModEmbed = findComponentByCodeLazy(".withFooter]:", "childrenMessageCo const ChannelMessage = findComponentByCodeLazy("renderSimpleAccessories)"); const SearchResultClasses = findByPropsLazy("message", "searchResult"); +const EmbedClasses = findByPropsLazy("embedAuthorIcon", "embedAuthor", "embedAuthor"); const messageLinkRegex = /(?{accessories} : null; } +function getChannelLabelAndIconUrl(channel: Channel) { + if (channel.isDM()) return ["Direct Message", IconUtils.getUserAvatarURL(UserStore.getUser(channel.recipients[0]))]; + if (channel.isGroupDM()) return ["Group DM", IconUtils.getChannelIconURL(channel)]; + return ["Server", IconUtils.getGuildIconURL(GuildStore.getGuild(channel.guild_id))]; +} + function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps): JSX.Element | null { - const guild = !channel.isDM() && GuildStore.getGuild(channel.guild_id); const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]); + const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel); - return - {channel.isDM() && Direct Message - } - {Parser.parse(channel.isDM() ? `<@${dmReceiver.id}>` : `<#${channel.id}>`)} - , - iconProxyURL: guild - ? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png` - : `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}` - } - }} - renderDescription={() => ( -
- -
- )} - />; + return ( + + {channelLabel} - + {Parser.parse(channel.isDM() ? `<@${dmReceiver.id}>` : `<#${channel.id}>`)} + , + iconProxyURL: iconUrl + } + }} + renderDescription={() => ( +
+ +
+ )} + /> + ); } function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { @@ -317,15 +325,20 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { const images = getImages(message); const { parse } = Parser; + const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel); + return - {channel.isDM() - ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) - : parse(`<#${channel.id}>`) - } - {channel.isDM() && - Direct Message} + + {iconUrl && } + + {channelLabel} - + {channel.isDM() + ? Parser.parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) + : Parser.parse(`<#${channel.id}>`) + } + } compact={compact} diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 226d000f1..40d5201cb 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -20,11 +20,10 @@ 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 { Avatar, ChannelStore, Clickable, IconUtils, 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"); @@ -71,7 +70,7 @@ export default definePlugin({ }} > diff --git a/src/plugins/serverProfile/GuildProfileModal.tsx b/src/plugins/serverProfile/GuildProfileModal.tsx index be1f2cd54..834367e0a 100644 --- a/src/plugins/serverProfile/GuildProfileModal.tsx +++ b/src/plugins/serverProfile/GuildProfileModal.tsx @@ -12,10 +12,9 @@ import { classes } from "@utils/misc"; import { ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import { findByPropsLazy, findExportedComponentLazy } from "@webpack"; -import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common"; +import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common"; import { Guild, User } from "discord-types/general"; -const IconUtils = findByPropsLazy("getGuildBannerURL"); const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper"); const FriendRow = findExportedComponentLazy("FriendRow"); @@ -65,10 +64,7 @@ function GuildProfileModal({ guild }: GuildProps) { const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo); - const bannerUrl = guild.banner && IconUtils.getGuildBannerURL({ - id: guild.id, - banner: guild.banner - }, true).replace(/\?size=\d+$/, "?size=1024"); + const bannerUrl = guild.banner && IconUtils.getGuildBannerURL(guild, true)!.replace(/\?size=\d+$/, "?size=1024"); const iconUrl = guild.icon && IconUtils.getGuildIconURL({ id: guild.id, @@ -89,7 +85,7 @@ function GuildProfileModal({ guild }: GuildProps) { )}
- {guild.icon + {iconUrl ? openImage(BannerStore.getUserAvatarURL(user, true))} + action={() => openImage(IconUtils.getUserAvatarURL(user, true))} icon={ImageIcon} /> {memberAvatar && ( openImage(BannerStore.getGuildMemberAvatarURLSimple({ + action={() => openImage(IconUtils.getGuildMemberAvatarURLSimple({ userId: user.id, avatar: memberAvatar, - guildId, + guildId: guildId!, canAnimate: true - }, true))} + }))} icon={ImageIcon} /> )} @@ -124,11 +122,11 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon id="view-icon" label="View Icon" action={() => - openImage(BannerStore.getGuildIconURL({ + openImage(IconUtils.getGuildIconURL({ id, icon, canAnimate: true - })) + })!) } icon={ImageIcon} /> @@ -138,10 +136,7 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon id="view-banner" label="View Banner" action={() => - openImage(BannerStore.getGuildBannerURL({ - id, - banner, - }, true)) + openImage(IconUtils.getGuildBannerURL(guild, true)!) } icon={ImageIcon} /> diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index 246659146..2005581a1 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import { Guild, GuildMember } from "discord-types/general"; import type { ReactNode } from "react"; import type { FluxEvents } from "./fluxEvents"; @@ -182,3 +183,47 @@ export interface NavigationRouter { getLastRouteChangeSource(): any; getLastRouteChangeSourceLocationStack(): any; } + +export interface IconUtils { + getUserAvatarURL(user: User, canAnimate?: boolean, size?: number, format?: string): string; + getDefaultAvatarURL(id: string, discriminator?: string): string; + getUserBannerURL(data: { id: string, banner: string, canAnimate?: boolean, size: number; }): string | undefined; + getAvatarDecorationURL(dara: { avatarDecoration: string, size: number; canCanimate?: boolean; }): string | undefined; + + getGuildMemberAvatarURL(member: GuildMember, canAnimate?: string): string | null; + getGuildMemberAvatarURLSimple(data: { guildId: string, userId: string, avatar: string, canAnimate?: boolean; size?: number; }): string; + getGuildMemberBannerURL(data: { id: string, guildId: string, banner: string, canAnimate?: boolean, size: number; }): string | undefined; + + getGuildIconURL(data: { id: string, icon?: string, size?: number, canAnimate?: boolean; }): string | undefined; + getGuildBannerURL(guild: Guild, canAnimate?: boolean): string | null; + + getChannelIconURL(data: { id: string; icon?: string; applicationId?: string; size?: number; }): string | undefined; + getEmojiURL(data: { id: string, animated: boolean, size: number, forcePNG?: boolean; }): string; + + hasAnimatedGuildIcon(guild: Guild): boolean; + isAnimatedIconHash(hash: string): boolean; + + getGuildSplashURL: any; + getGuildDiscoverySplashURL: any; + getGuildHomeHeaderURL: any; + getResourceChannelIconURL: any; + getNewMemberActionIconURL: any; + getGuildTemplateIconURL: any; + getApplicationIconURL: any; + getGameAssetURL: any; + getVideoFilterAssetURL: any; + + getGuildMemberAvatarSource: any; + getUserAvatarSource: any; + getGuildSplashSource: any; + getGuildDiscoverySplashSource: any; + makeSource: any; + getGameAssetSource: any; + getGuildIconSource: any; + getGuildTemplateIconSource: any; + getGuildBannerSource: any; + getGuildHomeHeaderSource: any; + getChannelIconSource: any; + getApplicationIconSource: any; + getAnimatableSourceWithFallback: any; +} diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index c62f745a9..7060573d5 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -137,3 +137,5 @@ export const { persist: zustandPersist }: typeof import("zustand/middleware") = export const MessageActions = findByPropsLazy("editMessage", "sendMessage"); export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal"); export const InviteActions = findByPropsLazy("resolveInvite"); + +export const IconUtils: t.IconUtils = findByPropsLazy("getGuildBannerURL", "getUserAvatarURL"); From 7b960716439cba093202f9ba37dd6b258be39017 Mon Sep 17 00:00:00 2001 From: Syncx <47534062+Syncxv@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:52:09 +1100 Subject: [PATCH 070/356] fix ImageZoom patch (#2181) --- src/plugins/imageZoom/components/Magnifier.tsx | 11 ++++++----- src/plugins/imageZoom/index.tsx | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/plugins/imageZoom/components/Magnifier.tsx b/src/plugins/imageZoom/components/Magnifier.tsx index 6a3fc05a0..816717350 100644 --- a/src/plugins/imageZoom/components/Magnifier.tsx +++ b/src/plugins/imageZoom/components/Magnifier.tsx @@ -123,14 +123,13 @@ export const Magnifier: React.FC = ({ instance, size: initialSiz waitFor(() => instance.state.readyState === "READY", () => { const elem = document.getElementById(ELEMENT_ID) as HTMLDivElement; element.current = elem; - elem.firstElementChild!.setAttribute("draggable", "false"); + elem.querySelector("img,video")?.setAttribute("draggable", "false"); if (instance.props.animated) { originalVideoElementRef.current = elem!.querySelector("video")!; originalVideoElementRef.current.addEventListener("timeupdate", syncVideos); - setReady(true); - } else { - setReady(true); } + + setReady(true); }); document.addEventListener("keydown", onKeyDown); document.addEventListener("keyup", onKeyUp); @@ -155,7 +154,9 @@ export const Magnifier: React.FC = ({ instance, size: initialSiz if (!ready) return null; - const box = element.current!.getBoundingClientRect(); + const box = element.current?.getBoundingClientRect(); + + if (!box) return null; return (
Date: Sun, 18 Feb 2024 17:42:26 +0100 Subject: [PATCH 071/356] MuteNewGuild -> NewGuildSettings; add 'show all channels' option (#2065) Co-authored-by: MopigamesYT Co-authored-by: V --- .../index.tsx | 25 +++++++++++++------ src/utils/constants.ts | 4 +++ 2 files changed, 22 insertions(+), 7 deletions(-) rename src/plugins/{muteNewGuild => newGuildSettings}/index.tsx (72%) diff --git a/src/plugins/muteNewGuild/index.tsx b/src/plugins/newGuildSettings/index.tsx similarity index 72% rename from src/plugins/muteNewGuild/index.tsx rename to src/plugins/newGuildSettings/index.tsx index 08c558a95..ff6f1c261 100644 --- a/src/plugins/muteNewGuild/index.tsx +++ b/src/plugins/newGuildSettings/index.tsx @@ -16,16 +16,18 @@ * along with this program. If not, see . */ -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings,migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; const { updateGuildNotificationSettings } = findByPropsLazy("updateGuildNotificationSettings"); +const { toggleShowAllChannels } = findByPropsLazy("toggleShowAllChannels"); +const { isOptInEnabledForGuild } = findByPropsLazy("isOptInEnabledForGuild"); const settings = definePluginSettings({ guild: { - description: "Mute Guild", + description: "Mute Guild automatically", type: OptionType.BOOLEAN, default: true }, @@ -38,13 +40,20 @@ const settings = definePluginSettings({ description: "Suppress All Role @mentions", type: OptionType.BOOLEAN, default: true + }, + showAllChannels: { + description: "Show all channels automatically", + type: OptionType.BOOLEAN, + default: true } }); +migratePluginSettings("NewGuildSettings", "MuteNewGuild"); export default definePlugin({ - name: "MuteNewGuild", - description: "Mutes newly joined guilds", - authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince], + name: "NewGuildSettings", + description: "Automatically mute new servers and change various other settings upon joining", + tags: ["MuteNewGuild", "mute", "server"], + authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi], patches: [ { find: ",acceptInvite(", @@ -70,7 +79,9 @@ export default definePlugin({ muted: settings.store.guild, suppress_everyone: settings.store.everyone, suppress_roles: settings.store.role - } - ); + }); + if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) { + toggleShowAllChannels(guildId); + } } }); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 55af93605..4b8caf825 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -399,6 +399,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "maisy", id: 257109471589957632n, }, + Mopi: { + name: "Mopi", + id: 1022189106614243350n + }, Grzesiek11: { name: "Grzesiek11", id: 368475654662127616n, From 604f4c49aff587a6f0d5038c50123c040d0c2527 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sun, 18 Feb 2024 18:22:33 +0100 Subject: [PATCH 072/356] VoiceMessages: Add warning if audio file is not OggOpus --- src/plugins/voiceMessages/index.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index f4898de68..2393ef2b6 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -20,13 +20,15 @@ import "./styles.css"; import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { Microphone } from "@components/Icons"; +import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; +import { Margins } from "@utils/margins"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import definePlugin from "@utils/types"; import { chooseFile } from "@utils/web"; import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { Button, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; +import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; import { ComponentType } from "react"; import { VoiceRecorderDesktop } from "./DesktopRecorder"; @@ -164,6 +166,11 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { fallbackValue: EMPTY_META, }); + const isUnsupportedFormat = blob && ( + !blob.type.startsWith("audio/ogg") + || blob.type.includes("codecs") && !blob.type.includes("opus") + ); + return ( @@ -200,6 +207,16 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { recording={isRecording} /> + {isUnsupportedFormat && ( + + Voice Messages have to be OggOpus to be playable on iOS. This file is {blob.type} so it will not be playable on iOS. + + + To fix it, first convert it to OggOpus, for example using the convertio web converter + + + )} + From f922f0bc0d4a4da12663b7ef86bf16cd17c59ba9 Mon Sep 17 00:00:00 2001 From: Manti <67705577+mantikafasi@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:13:25 +0300 Subject: [PATCH 073/356] fix reviewdb auth not working on userscript (#2194) --- src/plugins/reviewDB/auth.tsx | 2 +- src/plugins/reviewDB/reviewDbApi.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx index 136001b2d..4cd81f2ea 100644 --- a/src/plugins/reviewDB/auth.tsx +++ b/src/plugins/reviewDB/auth.tsx @@ -59,7 +59,7 @@ export function authorize(callback?: any) { const url = new URL(response.location); url.searchParams.append("clientMod", "vencord"); const res = await fetch(url, { - headers: new Headers({ Accept: "application/json" }) + headers: { Accept: "application/json" } }); if (!res.ok) { diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts index ec6d9ff3b..3fe6dd061 100644 --- a/src/plugins/reviewDB/reviewDbApi.ts +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -118,10 +118,10 @@ export async function addReview(review: any): Promise { export async function deleteReview(id: number): Promise { return await rdbRequest(`/users/${id}/reviews`, { method: "DELETE", - headers: new Headers({ + headers: { "Content-Type": "application/json", Accept: "application/json", - }), + }, body: JSON.stringify({ reviewid: id }) @@ -135,10 +135,10 @@ export async function deleteReview(id: number): Promise { export async function reportReview(id: number) { const res = await rdbRequest("/reports", { method: "PUT", - headers: new Headers({ + headers: { "Content-Type": "application/json", Accept: "application/json", - }), + }, body: JSON.stringify({ reviewid: id, }) @@ -150,10 +150,10 @@ export async function reportReview(id: number) { async function patchBlock(action: "block" | "unblock", userId: string) { const res = await rdbRequest("/blocks", { method: "PATCH", - headers: new Headers({ + headers: { "Content-Type": "application/json", Accept: "application/json", - }), + }, body: JSON.stringify({ action: action, discordId: userId @@ -180,9 +180,9 @@ export const unblockUser = (userId: string) => patchBlock("unblock", userId); export async function fetchBlocks(): Promise { const res = await rdbRequest("/blocks", { method: "GET", - headers: new Headers({ + headers: { Accept: "application/json", - }) + } }); if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`); From e3fd954512cb56cf05141f44465429530403a223 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 22 Feb 2024 21:31:15 -0300 Subject: [PATCH 074/356] Fix Decor patch --- src/plugins/decor/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index ce546d309..8cfd8c036 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -72,7 +72,7 @@ export default definePlugin({ replacement: [ // Add Decor avatar decoration hook to avatar decoration hook { - match: /(?<=TryItOut:\i}\),)(?<=user:(\i).+?)/, + match: /(?<=TryItOut:\i,guildId:\i}\),)(?<=user:(\i).+?)/, replace: "vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1)," }, // Use added hook From 2d8715adf01890d422f3c1471ce0f34c56e5adeb Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 23 Feb 2024 13:06:03 +0100 Subject: [PATCH 075/356] SuperReactionTweaks: only super react by default if user has nitro --- src/plugins/superReactionTweaks/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 0e58eb0a8..89197b4c3 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -7,6 +7,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; +import { UserStore } from "@webpack/common"; export const settings = definePluginSettings({ superReactByDefault: { @@ -49,7 +50,7 @@ export default definePlugin({ find: ".trackEmojiSearchEmpty,200", replacement: { match: /(\.trackEmojiSearchEmpty,200(?=.+?isBurstReaction:(\i).+?(\i===\i\.EmojiIntention.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/, - replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.settings.store.superReactByDefault&&${isReactionIntention})` + replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})` } } ], @@ -59,5 +60,9 @@ export default definePlugin({ if (settings.store.unlimitedSuperReactionPlaying) return true; if (playingCount <= settings.store.superReactionPlayingLimit) return true; return false; + }, + + get shouldSuperReactByDefault() { + return settings.store.superReactByDefault && UserStore.getCurrentUser().premiumType != null; } }); From 414184ef25387248e9eb810ea725dfab9d3ef82a Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Mon, 26 Feb 2024 12:51:09 +1300 Subject: [PATCH 076/356] ImageZoom: negate the border offsetting the lens (#2117) Co-authored-by: Lewis Crichton --- src/plugins/imageZoom/styles.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/imageZoom/styles.css b/src/plugins/imageZoom/styles.css index 51e225c05..c3776d90e 100644 --- a/src/plugins/imageZoom/styles.css +++ b/src/plugins/imageZoom/styles.css @@ -9,6 +9,9 @@ box-shadow: inset 0 0 10px 2px grey; filter: drop-shadow(0 0 2px grey); pointer-events: none; + + /* negate the border offsetting the lens */ + margin: -2px; } .vc-imgzoom-square { From 9958f5a2ea651a7890a613a9a6661654548a0a8a Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 27 Feb 2024 05:04:54 -0300 Subject: [PATCH 077/356] Fix ReviewDB --- src/plugins/reviewDB/components/ReviewsView.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index 64cea1815..eea92bb81 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -32,6 +32,7 @@ const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms"); const { ChatInputTypes } = findByPropsLazy("ChatInputTypes"); const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); +const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer"); interface UserProps { discordId: string; @@ -125,19 +126,7 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { const inputType = ChatInputTypes.FORM; inputType.disableAutoFocus = true; - const channel = { - flags_: 256, - guild_id_: null, - id: "0", - getGuildId: () => null, - isPrivate: () => true, - isActiveThread: () => false, - isArchivedLockedThread: () => false, - isDM: () => true, - roles: { "0": { permissions: 0n } }, - getRecipientId: () => "0", - hasFlag: () => false, - }; + const channel = createChannelRecordFromServer({ id: "0", type: 1 }); return ( <> From 5e7b4e9c92ac52e2a3b839390643af0db2fb919e Mon Sep 17 00:00:00 2001 From: Av32000 <59660601+Av32000@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:27:34 +0100 Subject: [PATCH 078/356] SpotifyControls: export album cover as CSS variable for themes (#2197) --vc-spotify-track-image --- src/plugins/spotifyControls/PlayerComponent.tsx | 6 +++++- src/plugins/spotifyControls/index.tsx | 2 +- src/utils/constants.ts | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index f2370906b..8b3f04bf2 100644 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ b/src/plugins/spotifyControls/PlayerComponent.tsx @@ -371,6 +371,10 @@ export function Player() { if (!track || !device?.is_active || shouldHide) return null; + const exportTrackImageStyle = { + "--vc-spotify-track-image": `url(${track?.album?.image?.url || ""})`, + } as React.CSSProperties; + return ( (
@@ -378,7 +382,7 @@ export function Player() {

Check the console for errors

)}> -
+
diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx index cfb352efe..d7e4f6454 100644 --- a/src/plugins/spotifyControls/index.tsx +++ b/src/plugins/spotifyControls/index.tsx @@ -31,7 +31,7 @@ function toggleHoverControls(value: boolean) { export default definePlugin({ name: "SpotifyControls", description: "Adds a Spotify player above the account panel", - authors: [Devs.Ven, Devs.afn, Devs.KraXen72], + authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000], options: { hoverControls: { description: "Show controls on hover", diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 4b8caf825..d66bdc826 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -414,6 +414,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ coolelectronics: { name: "coolelectronics", id: 696392247205298207n, + }, + Av32000: { + name: "Av32000", + id: 593436735380127770n, } } satisfies Record); From b9d0a1c563dfdc0a7b06da5eb7b037314c8260e1 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 27 Feb 2024 11:47:28 +0100 Subject: [PATCH 079/356] SpotifyControls: fix seekbar grabber alignment --- src/plugins/spotifyControls/spotifyStyles.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/plugins/spotifyControls/spotifyStyles.css b/src/plugins/spotifyControls/spotifyStyles.css index 9e585ebec..72383c3e8 100644 --- a/src/plugins/spotifyControls/spotifyStyles.css +++ b/src/plugins/spotifyControls/spotifyStyles.css @@ -170,9 +170,16 @@ /* these importants are necessary, it applies a width and height through inline styles */ height: 10px !important; width: 10px !important; + margin-top: 4px; background-color: var(--interactive-normal); border-color: var(--interactive-normal); color: var(--interactive-normal); + opacity: 0; + transition: opacity 0.1s; +} + +#vc-spotify-progress-bar:hover > [class^="slider"] [class^="grabber"] { + opacity: 1; } #vc-spotify-progress-text { From 27696ed62a8744bbf3f31c02497f238946106153 Mon Sep 17 00:00:00 2001 From: WackyModer <78763021+WackyModer@users.noreply.github.com> Date: Tue, 27 Feb 2024 03:31:51 -0800 Subject: [PATCH 080/356] whoReacted: fix reaction count being off by one (#2209) Co-authored-by: V --- src/plugins/whoReacted/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx index 4a2bdeeda..6d994be16 100644 --- a/src/plugins/whoReacted/index.tsx +++ b/src/plugins/whoReacted/index.tsx @@ -69,14 +69,14 @@ function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) { function makeRenderMoreUsers(users: User[]) { return function renderMoreUsers(_label: string, _count: number) { return ( - u.username).join(", ")} > + u.username).join(", ")} > {({ onMouseEnter, onMouseLeave }) => (
- +{users.length - 5} + +{users.length - 4}
)}
From ed5e1be7a4d50d612908fff35fa901e81330ce86 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:19:05 -0300 Subject: [PATCH 081/356] Add permissions checks for FakeNitro actions (#2160) Co-authored-by: Vendicated --- src/api/MessageEvents.ts | 10 +- src/plugins/_api/messageEvents.ts | 9 +- src/plugins/fakeNitro/{index.ts => index.tsx} | 121 +++++++++++++----- src/webpack/common/types/utils.d.ts | 1 + 4 files changed, 105 insertions(+), 36 deletions(-) rename src/plugins/fakeNitro/{index.ts => index.tsx} (89%) diff --git a/src/api/MessageEvents.ts b/src/api/MessageEvents.ts index 341b4e678..d6eba748f 100644 --- a/src/api/MessageEvents.ts +++ b/src/api/MessageEvents.ts @@ -74,7 +74,7 @@ export interface MessageExtra { } export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable; -export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; +export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; const sendListeners = new Set(); const editListeners = new Set(); @@ -84,7 +84,7 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec for (const listener of sendListeners) { try { const result = await listener(channelId, messageObj, extra); - if (result && result.cancel === true) { + if (result?.cancel) { return true; } } catch (e) { @@ -97,11 +97,15 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) { for (const listener of editListeners) { try { - await listener(channelId, messageId, messageObj); + const result = await listener(channelId, messageId, messageObj); + if (result?.cancel) { + return true; + } } catch (e) { MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e); } } + return false; } /** diff --git a/src/plugins/_api/messageEvents.ts b/src/plugins/_api/messageEvents.ts index bc5f5abf2..1b4a2d15a 100644 --- a/src/plugins/_api/messageEvents.ts +++ b/src/plugins/_api/messageEvents.ts @@ -25,10 +25,13 @@ export default definePlugin({ authors: [Devs.Arjix, Devs.hunt, Devs.Ven], patches: [ { - find: '"MessageActionCreators"', + find: ".Messages.EDIT_TEXTAREA_HELP", replacement: { - match: /async editMessage\(.+?\)\{/, - replace: "$&await Vencord.Api.MessageEvents._handlePreEdit(...arguments);" + match: /(?<=,channel:\i\}\)\.then\().+?(?=return \i\.content!==this\.props\.message\.content&&\i\((.+?)\))/, + replace: (match, args) => "" + + `async ${match}` + + `if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` + + "return Promise.resolve({shoudClear:true,shouldRefocus:true});" } }, { diff --git a/src/plugins/fakeNitro/index.ts b/src/plugins/fakeNitro/index.tsx similarity index 89% rename from src/plugins/fakeNitro/index.ts rename to src/plugins/fakeNitro/index.tsx index 560cae381..b9932d291 100644 --- a/src/plugins/fakeNitro/index.ts +++ b/src/plugins/fakeNitro/index.tsx @@ -17,14 +17,14 @@ */ import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings, Settings } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; import { getCurrentGuild } from "@utils/discord"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; -import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; +import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import type { Message } from "discord-types/general"; import { applyPalette, GIFEncoder, quantize } from "gifenc"; import type { ReactElement, ReactNode } from "react"; @@ -51,8 +51,6 @@ const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsA const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); -const USE_EXTERNAL_EMOJIS = 1n << 18n; -const USE_EXTERNAL_STICKERS = 1n << 37n; const enum EmojiIntentions { REACTION = 0, @@ -162,8 +160,23 @@ const settings = definePluginSettings({ description: "Whether to use hyperlinks when sending fake emojis and stickers", type: OptionType.BOOLEAN, default: true - } -}); + }, +}).withPrivateSettings<{ + disableEmbedPermissionCheck: boolean; +}>(); + +function hasPermission(channelId: string, permission: bigint) { + const channel = ChannelStore.getChannel(channelId); + + if (!channel || channel.isPrivate()) return true; + + return PermissionStore.can(permission, channel); +} + +const hasExternalEmojiPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_EMOJIS); +const hasExternalStickerPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_STICKERS); +const hasEmbedPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.EMBED_LINKS); +const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.ATTACH_FILES); export default definePlugin({ name: "FakeNitro", @@ -696,22 +709,6 @@ export default definePlugin({ } }, - hasPermissionToUseExternalEmojis(channelId: string): boolean { - const channel = ChannelStore.getChannel(channelId); - - if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true; - - return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel); - }, - - hasPermissionToUseExternalStickers(channelId: string) { - const channel = ChannelStore.getChannel(channelId); - - if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true; - - return PermissionStore.can(USE_EXTERNAL_STICKERS, channel); - }, - getStickerLink(stickerId: string) { return `https://media.discordapp.net/stickers/${stickerId}.png?size=${settings.store.stickerSize}`; }, @@ -722,7 +719,7 @@ export default definePlugin({ const { frames, width, height } = await parseURL(stickerLink); const gif = GIFEncoder(); - const resolution = Settings.plugins.FakeNitro.stickerSize; + const resolution = settings.store.stickerSize; const canvas = document.createElement("canvas"); canvas.width = resolution; @@ -783,9 +780,38 @@ export default definePlugin({ return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " "; } - this.preSend = addPreSendListener((channelId, messageObj, extra) => { + function cannotEmbedNotice() { + return new Promise(resolve => { + Alerts.show({ + title: "Hold on!", + body:
+ + You are trying to send/edit a message that contains a FakeNitro emoji or sticker + , however you do not have permissions to embed links in the current channel. + Are you sure you want to send this message? Your FakeNitro items will appear as a link only. + + + You can disable this notice in the plugin settings. + +
, + confirmText: "Send Anyway", + cancelText: "Cancel", + secondaryConfirmText: "Do not show again", + onConfirm: () => resolve(true), + onCloseCallback: () => setImmediate(() => resolve(false)), + onConfirmSecondary() { + settings.store.disableEmbedPermissionCheck = true; + resolve(true); + } + }); + }); + } + + this.preSend = addPreSendListener(async (channelId, messageObj, extra) => { const { guildId } = this; + let hasBypass = false; + stickerBypass: { if (!s.enableStickerBypass) break stickerBypass; @@ -798,7 +824,7 @@ export default definePlugin({ if ("pack_id" in sticker) break stickerBypass; - const canUseStickers = this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId); + const canUseStickers = this.canUseStickers && hasExternalStickerPerms(channelId); if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId)) break stickerBypass; @@ -812,9 +838,24 @@ export default definePlugin({ } if (sticker.format_type === StickerType.APNG) { - this.sendAnimatedSticker(link, sticker.id, channelId); + if (!hasAttachmentPerms(channelId)) { + Alerts.show({ + title: "Hold on!", + body:
+ + You cannot send this message because it contains an animated FakeNitro sticker, + and you do not have permissions to attach files in the current channel. Please remove the sticker to proceed. + +
+ }); + } else { + this.sendAnimatedSticker(link, sticker.id, channelId); + } + return { cancel: true }; } else { + hasBypass = true; + const url = new URL(link); url.searchParams.set("name", sticker.name); @@ -824,13 +865,15 @@ export default definePlugin({ } if (s.enableEmojiBypass) { - const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId); + const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId); for (const emoji of messageObj.validNonShortcutEmojis) { if (!emoji.require_colons) continue; if (emoji.available !== false && canUseEmotes) continue; if (emoji.guildId === guildId && !emoji.animated) continue; + hasBypass = true; + const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; const url = new URL(emoji.url); @@ -843,16 +886,24 @@ export default definePlugin({ } } + if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) { + if (!await cannotEmbedNotice()) { + return { cancel: true }; + } + } + return { cancel: false }; }); - this.preEdit = addPreEditListener((channelId, __, messageObj) => { + this.preEdit = addPreEditListener(async (channelId, __, messageObj) => { if (!s.enableEmojiBypass) return; - const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId); - const { guildId } = this; + let hasBypass = false; + + const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId); + messageObj.content = messageObj.content.replace(/(?/ig, (emojiStr, emojiId, offset, origStr) => { const emoji = EmojiStore.getCustomEmojiById(emojiId); if (emoji == null) return emojiStr; @@ -860,12 +911,22 @@ export default definePlugin({ if (emoji.available !== false && canUseEmotes) return emojiStr; if (emoji.guildId === guildId && !emoji.animated) return emojiStr; + hasBypass = true; + const url = new URL(emoji.url); url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("name", emoji.name); return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; }); + + if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) { + if (!await cannotEmbedNotice()) { + return { cancel: true }; + } + } + + return { cancel: false }; }); }, diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index 2005581a1..3d1c0eea6 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -59,6 +59,7 @@ export interface Alerts { onCancel?(): void; onConfirm?(): void; onConfirmSecondary?(): void; + onCloseCallback?(): void; }): void; /** This is a noop, it does nothing. */ close(): void; From 76de8c424e13c60d63d78894c1894a0ab2390e81 Mon Sep 17 00:00:00 2001 From: Lualt <58912038+LualtOfficial@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:27:37 +0100 Subject: [PATCH 082/356] feat(plugin) FakeNitro: Allow customising hyperlink text (#2192) Co-authored-by: Vendicated --- src/plugins/fakeNitro/index.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index b9932d291..20125c7d1 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -161,6 +161,11 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true }, + hyperLinkText: { + description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji name.", + type: OptionType.STRING, + default: "{{NAME}}" + } }).withPrivateSettings<{ disableEmbedPermissionCheck: boolean; }>(); @@ -880,8 +885,10 @@ export default definePlugin({ url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("name", emoji.name); + const linkText = s.hyperLinkText.replaceAll("{{NAME}}", emoji.name); + messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`; + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`; }); } } @@ -917,7 +924,9 @@ export default definePlugin({ url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("name", emoji.name); - return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; + const linkText = s.hyperLinkText.replaceAll("{{NAME}}", emoji.name); + + return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; }); if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) { From 1afa185f578dc54bb3a01a7fcc03d6016870971a Mon Sep 17 00:00:00 2001 From: Andrei Neacsu <58575812+prycaustic@users.noreply.github.com> Date: Tue, 27 Feb 2024 06:30:27 -0600 Subject: [PATCH 083/356] LastfmRichPresence: Add an option to hide the Last.fm logo (#2189) Co-authored-by: V --- src/plugins/lastfm/index.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index 179b8260c..5dfec8a32 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -170,6 +170,11 @@ const settings = definePluginSettings({ } ], }, + showLastFmLogo: { + description: "show the Last.fm logo by the album cover", + type: OptionType.BOOLEAN, + default: true, + } }); export default definePlugin({ @@ -276,8 +281,10 @@ export default definePlugin({ { large_image: await getApplicationAsset(largeImage), large_text: trackData.album || undefined, - small_image: await getApplicationAsset("lastfm-small"), - small_text: "Last.fm", + ...(settings.store.showLastFmLogo && { + small_image: await getApplicationAsset("lastfm-small"), + small_text: "Last.fm" + }), } : { large_image: await getApplicationAsset("lastfm-large"), large_text: trackData.album || undefined, From 8ccd731aee3a7c797b70a285c6023bcdc8d7d2b1 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 27 Feb 2024 13:41:55 +0100 Subject: [PATCH 084/356] bump to v1.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ffac7ff5..dde55d311 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.9", + "version": "1.7.0", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From e0166ef1e6f3962c14e7f5b902cb03675919eec3 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 27 Feb 2024 23:42:57 -0300 Subject: [PATCH 085/356] Fix FakeNitro patch and message content patch error --- src/plugins/fakeNitro/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 20125c7d1..a864a3a67 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -369,8 +369,8 @@ export default definePlugin({ predicate: () => settings.store.transformEmojis, replacement: { // Add the fake nitro emoji notice - match: /(?<=isDiscoverable:\i,emojiComesFromCurrentGuild:\i,.+?}=(\i).+?;)(.*?return )(.{0,1000}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(?=},)/, - replace: (_, props, rest, reactNode) => `let{fakeNitroNode}=${props};${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!fakeNitroNode?.fake)` + match: /(?<=emojiDescription:)(\i)(?<=\1=\i\((\i)\).+?)/, + replace: (_, reactNode, props) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!${props}?.fakeNitroNode?.fake)` } }, // Allow using custom app icons @@ -474,7 +474,7 @@ export default definePlugin({ if (typeof firstContent === "string") { content[0] = firstContent.trimStart(); content[0] || content.shift(); - } else if (firstContent?.type === "span") { + } else if (typeof firstContent?.props.children === "string") { firstContent.props.children = firstContent.props.children.trimStart(); firstContent.props.children || content.shift(); } @@ -484,7 +484,7 @@ export default definePlugin({ if (typeof lastContent === "string") { content[lastIndex] = lastContent.trimEnd(); content[lastIndex] || content.pop(); - } else if (lastContent?.type === "span") { + } else if (typeof firstContent?.props.children === "string") { lastContent.props.children = lastContent.props.children.trimEnd(); lastContent.props.children || content.pop(); } From 7de54a294f0e64c314c7feac087eb4fe6bd03b88 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 27 Feb 2024 23:43:03 -0300 Subject: [PATCH 086/356] Fix NotificationVolume --- src/plugins/notificationVolume/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/notificationVolume/index.ts b/src/plugins/notificationVolume/index.ts index 50eabee73..bc3c7539d 100644 --- a/src/plugins/notificationVolume/index.ts +++ b/src/plugins/notificationVolume/index.ts @@ -27,8 +27,8 @@ export default definePlugin({ { find: "_ensureAudio(){", replacement: { - match: /onloadeddata=\(\)=>\{.\.volume=/, - replace: "$&$self.settings.store.notificationVolume/100*" + match: /(?=Math\.min\(\i\.\i\.getOutputVolume\(\)\/100)/, + replace: "$self.settings.store.notificationVolume/100*" }, }, ], From da50c7a19b1755c1827d4a3bb6c4651fe69551c7 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 28 Feb 2024 20:02:37 +0100 Subject: [PATCH 087/356] MemberCount: Also add to server tooltip; refactor code --- src/plugins/memberCount/MemberCount.tsx | 66 +++++++++++ .../memberCount/OnlineMemberCountStore.ts | 52 +++++++++ src/plugins/memberCount/index.tsx | 107 +++++------------- src/plugins/memberCount/style.css | 44 +++++++ 4 files changed, 189 insertions(+), 80 deletions(-) create mode 100644 src/plugins/memberCount/MemberCount.tsx create mode 100644 src/plugins/memberCount/OnlineMemberCountStore.ts create mode 100644 src/plugins/memberCount/style.css diff --git a/src/plugins/memberCount/MemberCount.tsx b/src/plugins/memberCount/MemberCount.tsx new file mode 100644 index 000000000..50665353e --- /dev/null +++ b/src/plugins/memberCount/MemberCount.tsx @@ -0,0 +1,66 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { getCurrentChannel } from "@utils/discord"; +import { SelectedChannelStore, Tooltip, useEffect, useStateFromStores } from "@webpack/common"; + +import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat } from "."; +import { OnlineMemberCountStore } from "./OnlineMemberCountStore"; + +export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) { + const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); + + const guildId = isTooltip ? tooltipGuildId! : currentChannel.guild_id; + + const totalCount = useStateFromStores( + [GuildMemberCountStore], + () => GuildMemberCountStore.getMemberCount(guildId) + ); + + let onlineCount = useStateFromStores( + [OnlineMemberCountStore], + () => OnlineMemberCountStore.getCount(guildId) + ); + + const { groups } = useStateFromStores( + [ChannelMemberStore], + () => ChannelMemberStore.getProps(guildId, currentChannel.id) + ); + + if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) { + onlineCount = groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0); + } + + useEffect(() => { + OnlineMemberCountStore.ensureCount(guildId); + }, [guildId]); + + if (totalCount == null) + return null; + + const formattedOnlineCount = onlineCount != null ? numberFormat(onlineCount) : "?"; + + return ( +
+ + {props => ( +
+ + {formattedOnlineCount} +
+ )} +
+ + {props => ( +
+ + {numberFormat(totalCount)} +
+ )} +
+
+ ); +} diff --git a/src/plugins/memberCount/OnlineMemberCountStore.ts b/src/plugins/memberCount/OnlineMemberCountStore.ts new file mode 100644 index 000000000..8790f5e29 --- /dev/null +++ b/src/plugins/memberCount/OnlineMemberCountStore.ts @@ -0,0 +1,52 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { proxyLazy } from "@utils/lazy"; +import { sleep } from "@utils/misc"; +import { Queue } from "@utils/Queue"; +import { Flux, FluxDispatcher, GuildChannelStore, PrivateChannelsStore } from "@webpack/common"; + +export const OnlineMemberCountStore = proxyLazy(() => { + const preloadQueue = new Queue(); + + const onlineMemberMap = new Map(); + + class OnlineMemberCountStore extends Flux.Store { + getCount(guildId: string) { + return onlineMemberMap.get(guildId); + } + + async _ensureCount(guildId: string) { + if (onlineMemberMap.has(guildId)) return; + + await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id); + } + + ensureCount(guildId: string) { + if (onlineMemberMap.has(guildId)) return; + + preloadQueue.push(() => + this._ensureCount(guildId) + .then( + () => sleep(200), + () => sleep(200) + ) + ); + } + } + + return new OnlineMemberCountStore(FluxDispatcher, { + GUILD_MEMBER_LIST_UPDATE({ guildId, groups }: { guildId: string, groups: { count: number; id: string; }[]; }) { + onlineMemberMap.set( + guildId, + groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0) + ); + }, + ONLINE_GUILD_MEMBER_COUNT_UPDATE({ guildId, count }) { + onlineMemberMap.set(guildId, count); + } + }); +}); diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index d9cd548e9..eb4ce372c 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -16,101 +16,48 @@ * along with this program. If not, see . */ +import "./style.css"; + +import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Flex } from "@components/Flex"; import { Devs } from "@utils/constants"; -import { getCurrentChannel } from "@utils/discord"; import definePlugin from "@utils/types"; import { findStoreLazy } from "@webpack"; -import { SelectedChannelStore, Tooltip, useStateFromStores } from "@webpack/common"; import { FluxStore } from "@webpack/types"; -const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; }; -const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & { +import { MemberCount } from "./MemberCount"; + +export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; }; +export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & { getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; }; }; const sharedIntlNumberFormat = new Intl.NumberFormat(); -const numberFormat = (value: number) => sharedIntlNumberFormat.format(value); - -function MemberCount() { - const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); - const { groups } = useStateFromStores( - [ChannelMemberStore], - () => ChannelMemberStore.getProps(guildId, channelId) - ); - const total = useStateFromStores( - [GuildMemberCountStore], - () => GuildMemberCountStore.getMemberCount(guildId) - ); - - if (total == null) - return null; - - const online = - (groups.length === 1 && groups[0].id === "unknown") - ? 0 - : groups.reduce((count, curr) => count + (curr.id === "offline" ? 0 : curr.count), 0); - - return ( - - - {props => ( -
- - {numberFormat(online)} -
- )} -
- - {props => ( -
- - {numberFormat(total)} -
- )} -
-
- ); -} +export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value); +export const cl = classNameFactory("vc-membercount-"); export default definePlugin({ name: "MemberCount", - description: "Shows the amount of online & total members in the server member list", + description: "Shows the amount of online & total members in the server member list and tooltip", authors: [Devs.Ven, Devs.Commandtechno], - patches: [{ - find: "{isSidebarVisible:", - replacement: { - match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, - replace: ":[$1?.startsWith('members')?$self.render():null,$2" + patches: [ + { + find: "{isSidebarVisible:", + replacement: { + match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, + replace: ":[$1?.startsWith('members')?$self.render():null,$2" + } + }, + { + find: ".invitesDisabledTooltip", + replacement: { + match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/, + replace: ",$self.renderTooltip(arguments[0].guild)]" + } } - }], + ], - render: ErrorBoundary.wrap(MemberCount, { noop: true }) + render: ErrorBoundary.wrap(MemberCount, { noop: true }), + renderTooltip: ErrorBoundary.wrap(guild => , { noop: true }) }); diff --git a/src/plugins/memberCount/style.css b/src/plugins/memberCount/style.css new file mode 100644 index 000000000..f43bff830 --- /dev/null +++ b/src/plugins/memberCount/style.css @@ -0,0 +1,44 @@ +.vc-membercount-widget { + display: flex; + align-content: center; + + --color-online: var(--green-360); + --color-total: var(--primary-400); +} + +.vc-membercount-tooltip { + margin-top: 0.25em; + margin-left: 2px; +} + +.vc-membercount-member-list { + justify-content: center; + margin-top: 1em; + padding-inline: 1em; +} + +.vc-membercount-online { + color: var(--color-online); +} + +.vc-membercount-total { + color: var(--color-total); +} + +.vc-membercount-online-dot { + background-color: var(--color-online); + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 0.5em; +} + +.vc-membercount-total-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + border: 3px solid var(--color-total); + margin: 0 0.5em 0 1em; +} From 3ebde1aae8d54d96a9548911f4c8ad442092ea82 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 1 Mar 2024 00:18:09 +0100 Subject: [PATCH 088/356] fix some minor bugs --- scripts/generateReport.ts | 3 ++- src/plugins/fakeNitro/index.tsx | 4 ++-- src/webpack/webpack.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 0a17e8d7e..33b099ef8 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -428,10 +428,11 @@ function runTime(token: string) { if (searchType === "findComponent") method = "find"; if (searchType === "findExportedComponent") method = "findByProps"; - if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") { + if (searchType === "waitFor" || searchType === "waitForComponent") { if (typeof args[0] === "string") method = "findByProps"; else method = "find"; } + if (searchType === "waitForStore") method = "findStore"; try { let result: any; diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index a864a3a67..1cf1e536f 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -474,7 +474,7 @@ export default definePlugin({ if (typeof firstContent === "string") { content[0] = firstContent.trimStart(); content[0] || content.shift(); - } else if (typeof firstContent?.props.children === "string") { + } else if (typeof firstContent?.props?.children === "string") { firstContent.props.children = firstContent.props.children.trimStart(); firstContent.props.children || content.shift(); } @@ -484,7 +484,7 @@ export default definePlugin({ if (typeof lastContent === "string") { content[lastIndex] = lastContent.trimEnd(); content[lastIndex] || content.pop(); - } else if (typeof firstContent?.props.children === "string") { + } else if (typeof lastContent?.props?.children === "string") { lastContent.props.children = lastContent.props.children.trimEnd(); lastContent.props.children || content.pop(); } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index d65f57fcb..a68890a83 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -83,8 +83,8 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { return true; } +let devToolsOpen = false; if (IS_DEV && IS_DISCORD_DESKTOP) { - var devToolsOpen = false; // At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed setTimeout(() => { DiscordNative/* just to make sure */?.window.setDevtoolsCallbacks(() => devToolsOpen = true, () => devToolsOpen = false); From 9179f55bf241453cca020e2d8d7e59929a2028b2 Mon Sep 17 00:00:00 2001 From: "[object Object]" Date: Thu, 29 Feb 2024 16:26:32 -0800 Subject: [PATCH 089/356] fix Vencloud not working on UserScript (#2213) Co-authored-by: V --- browser/GMPolyfill.js | 2 +- src/components/VencordSettings/CloudTab.tsx | 4 +--- src/utils/cloud.tsx | 2 +- src/utils/settingsSync.ts | 12 +++++------- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/browser/GMPolyfill.js b/browser/GMPolyfill.js index f8801551e..387389ce6 100644 --- a/browser/GMPolyfill.js +++ b/browser/GMPolyfill.js @@ -62,7 +62,7 @@ function GM_fetch(url, opt) { resp.arrayBuffer = () => blobTo("arrayBuffer", blob); resp.text = () => blobTo("text", blob); resp.json = async () => JSON.parse(await blobTo("text", blob)); - resp.headers = new Headers(parseHeaders(resp.responseHeaders)); + resp.headers = parseHeaders(resp.responseHeaders); resp.ok = resp.status >= 200 && resp.status < 300; resolve(resp); }; diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx index 0392a451c..080dd8dd9 100644 --- a/src/components/VencordSettings/CloudTab.tsx +++ b/src/components/VencordSettings/CloudTab.tsx @@ -39,9 +39,7 @@ function validateUrl(url: string) { async function eraseAllData() { const res = await fetch(new URL("/v1/", getCloudUrl()), { method: "DELETE", - headers: new Headers({ - Authorization: await getCloudAuth() - }) + headers: { Authorization: await getCloudAuth() } }); if (!res.ok) { diff --git a/src/utils/cloud.tsx b/src/utils/cloud.tsx index f56c78dc5..508b1c7ef 100644 --- a/src/utils/cloud.tsx +++ b/src/utils/cloud.tsx @@ -106,7 +106,7 @@ export async function authorizeCloud() { try { const res = await fetch(location, { - headers: new Headers({ Accept: "application/json" }) + headers: { Accept: "application/json" } }); const { secret } = await res.json(); if (secret) { diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index ec32e2b1e..9a0f260af 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -118,10 +118,10 @@ export async function putCloudSettings(manual?: boolean) { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "PUT", - headers: new Headers({ + headers: { Authorization: await getCloudAuth(), "Content-Type": "application/octet-stream" - }), + }, body: deflateSync(new TextEncoder().encode(settings)) }); @@ -162,11 +162,11 @@ export async function getCloudSettings(shouldNotify = true, force = false) { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "GET", - headers: new Headers({ + headers: { Authorization: await getCloudAuth(), Accept: "application/octet-stream", "If-None-Match": Settings.cloud.settingsSyncVersion.toString() - }), + }, }); if (res.status === 404) { @@ -251,9 +251,7 @@ export async function deleteCloudSettings() { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "DELETE", - headers: new Headers({ - Authorization: await getCloudAuth() - }), + headers: { Authorization: await getCloudAuth() }, }); if (!res.ok) { From 1a1156e1ed73f71a79cbb8444990928731dbfb44 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:09:55 -0300 Subject: [PATCH 090/356] Add more settings to IgnoreActivities (#2153) --- src/plugins/ignoreActivities/index.tsx | 148 +++++++++++++++++++++++-- 1 file changed, 138 insertions(+), 10 deletions(-) diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 4e747f363..c04ce1c56 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -5,12 +5,14 @@ */ import * as DataStore from "@api/DataStore"; -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; +import { Flex } from "@components/Flex"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import { Margins } from "@utils/margins"; +import definePlugin, { OptionType } from "@utils/types"; import { findStoreLazy } from "@webpack"; -import { StatusSettingsStores, Tooltip } from "webpack/common"; +import { Button, Forms, showToast, StatusSettingsStores, TextInput, Toasts, Tooltip, useEffect, useState } from "webpack/common"; const enum ActivitiesTypes { Game, @@ -69,7 +71,113 @@ function handleActivityToggle(e: React.MouseEvent StatusSettingsStores.ShowCurrentGame.updateSetting(old => old); } -const settings = definePluginSettings({}).withPrivateSettings<{ +function ImportCustomRPCComponent() { + return ( + + Import the application id of the CustomRPC plugin to the allowed list +
+ +
+
+ ); +} + +let allowedIdsPushID: ((id: string) => boolean) | null = null; + +function AllowedIdsComponent(props: { setValue: (value: string) => void; }) { + const [allowedIds, setAllowedIds] = useState(settings.store.allowedIds ?? ""); + + allowedIdsPushID = (id: string) => { + const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean)); + + const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false); + + const ids = Array.from(currentIds).join(", "); + setAllowedIds(ids); + props.setValue(ids); + + return isAlreadyAdded; + }; + + useEffect(() => () => { + allowedIdsPushID = null; + }, []); + + function handleChange(newValue: string) { + setAllowedIds(newValue); + props.setValue(newValue); + } + + return ( + + Allowed List + Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC) + + + ); +} + +const settings = definePluginSettings({ + importCustomRPC: { + type: OptionType.COMPONENT, + description: "", + component: () => + }, + allowedIds: { + type: OptionType.COMPONENT, + description: "", + default: "", + onChange(newValue: string) { + const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean)); + settings.store.allowedIds = Array.from(ids).join(", "); + }, + component: props => + }, + ignorePlaying: { + type: OptionType.BOOLEAN, + description: "Ignore all playing activities (These are usually game and RPC activities)", + default: false + }, + ignoreStreaming: { + type: OptionType.BOOLEAN, + description: "Ignore all streaming activities", + default: false + }, + ignoreListening: { + type: OptionType.BOOLEAN, + description: "Ignore all listening activities (These are usually spotify activities)", + default: false + }, + ignoreWatching: { + type: OptionType.BOOLEAN, + description: "Ignore all watching activities", + default: false + }, + ignoreCompeting: { + type: OptionType.BOOLEAN, + description: "Ignore all competing activities (These are normally special game activities)", + default: false + } +}).withPrivateSettings<{ ignoredActivities: IgnoredActivity[]; }>(); @@ -77,10 +185,26 @@ function getIgnoredActivities() { return settings.store.ignoredActivities ??= []; } +function isActivityTypeIgnored(type: number, id?: string) { + if (id && settings.store.allowedIds.includes(id)) { + return false; + } + + switch (type) { + case 0: return settings.store.ignorePlaying; + case 1: return settings.store.ignoreStreaming; + case 2: return settings.store.ignoreListening; + case 3: return settings.store.ignoreWatching; + case 5: return settings.store.ignoreCompeting; + } + + return false; +} + export default definePlugin({ name: "IgnoreActivities", authors: [Devs.Nuckyz], - description: "Ignore activities from showing up on your status ONLY. You can configure which ones are ignored from the Registered Games and Activities tabs.", + description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.", settings, @@ -141,13 +265,17 @@ export default definePlugin({ }, isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) { - if (props.type === 0 || props.type === 3) { - if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id); - else { - const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; - if (exePath) return !getIgnoredActivities().some(activity => activity.id === exePath); + if (isActivityTypeIgnored(props.type, props.application_id)) return false; + + if (props.application_id != null) { + return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id); + } else { + const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; + if (exePath) { + return !getIgnoredActivities().some(activity => activity.id === exePath); } } + return true; }, From 806960f1c640822e190e2c7f355bda38d596601b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:15:19 -0300 Subject: [PATCH 091/356] ClientTheme: do not use lodash on start method --- src/plugins/clientTheme/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index 5d8cd4dc0..4e07daf42 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -12,7 +12,7 @@ import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import definePlugin, { OptionType, StartAt } from "@utils/types"; import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common"; +import { Button, Forms, useStateFromStores } from "@webpack/common"; const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); @@ -200,8 +200,8 @@ function captureOne(str, regex) { return (result === null) ? null : result[1]; } -function mapReject(arr, mapFunc, rejectFunc = _.isNull) { - return _.reject(arr.map(mapFunc), rejectFunc); +function mapReject(arr, mapFunc) { + return arr.map(mapFunc).filter(Boolean); } function updateColorVars(color: string) { From 553a48b6ce50a107e36e16f9914e723717b82c96 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:20:12 -0300 Subject: [PATCH 092/356] FakeNitro: Fix hyperlink text setting for stickers --- src/plugins/fakeNitro/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 1cf1e536f..5d662feed 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -162,7 +162,7 @@ const settings = definePluginSettings({ default: true }, hyperLinkText: { - description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji name.", + description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.", type: OptionType.STRING, default: "{{NAME}}" } @@ -864,7 +864,9 @@ export default definePlugin({ const url = new URL(link); url.searchParams.set("name", sticker.name); - messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${sticker.name}](${url})` : url}`; + const linkText = s.hyperLinkText.replaceAll("{{NAME}}", sticker.name); + + messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`; extra.stickers!.length = 0; } } From 23ff82fa627807fcbcdea03e88b3f21f65d007ca Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 1 Mar 2024 05:33:20 -0300 Subject: [PATCH 093/356] FakeNitro: Remove extra space in modal --- src/plugins/fakeNitro/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 5d662feed..8cf2492a8 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -791,8 +791,8 @@ export default definePlugin({ title: "Hold on!", body:
- You are trying to send/edit a message that contains a FakeNitro emoji or sticker - , however you do not have permissions to embed links in the current channel. + You are trying to send/edit a message that contains a FakeNitro emoji or sticker, + however you do not have permissions to embed links in the current channel. Are you sure you want to send this message? Your FakeNitro items will appear as a link only. From 4f0c0a12dc1832fbff3a71e05b0eb503e6b9c9c2 Mon Sep 17 00:00:00 2001 From: dolfies Date: Tue, 5 Mar 2024 17:38:49 -0500 Subject: [PATCH 094/356] feat(plugin): ResurrectHome (#2232) --- src/plugins/resurrectHome/README.md | 5 ++ src/plugins/resurrectHome/index.tsx | 125 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/plugins/resurrectHome/README.md create mode 100644 src/plugins/resurrectHome/index.tsx diff --git a/src/plugins/resurrectHome/README.md b/src/plugins/resurrectHome/README.md new file mode 100644 index 000000000..215ef816c --- /dev/null +++ b/src/plugins/resurrectHome/README.md @@ -0,0 +1,5 @@ +# ResurrectHome + +Brings back the phased out [Server Home](https://support.discord.com/hc/en-us/articles/6156116949911-Server-Home-Beta) feature! + +![](https://private-user-images.githubusercontent.com/47677887/309572891-a9ee7354-9e5e-4b81-8faf-304d9c44f512.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MDk0OTE5MTIsIm5iZiI6MTcwOTQ5MTYxMiwicGF0aCI6Ii80NzY3Nzg4Ny8zMDk1NzI4OTEtYTllZTczNTQtOWU1ZS00YjgxLThmYWYtMzA0ZDljNDRmNTEyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDAzMDMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwMzAzVDE4NDY1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBhYzUxMWY1MzQxNTA4NDE1MWU0YjAxNzM1NzI1YWJkMTNiZmNkNjRmYTRkZDg1ZDE5NzdkMjM0MGVjMDA0OWQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.TPYWPRWHTJstfviT9HOaBWFkbBhokyxiDC-gOVL2dqs) diff --git a/src/plugins/resurrectHome/index.tsx b/src/plugins/resurrectHome/index.tsx new file mode 100644 index 000000000..91ed87a02 --- /dev/null +++ b/src/plugins/resurrectHome/index.tsx @@ -0,0 +1,125 @@ +/* + * 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 . +*/ + +import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ContextMenuApi, Menu } from "@webpack/common"; + +const settings = definePluginSettings({ + forceServerHome: { + type: OptionType.BOOLEAN, + description: "Force the Server Guide to be the Server Home tab when it is enabled.", + default: false + } +}); + +const contextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (!props?.guild) return; + + const group = findGroupChildrenByChildId("hide-muted-channels", children); + + group?.unshift( + { + settings.store.forceServerHome = !settings.store.forceServerHome; + ContextMenuApi.closeContextMenu(); + }} + /> + + ); +}; + +export default definePlugin({ + name: "ResurrectHome", + description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.", + authors: [Devs.Dolfies, Devs.Nuckyz], + settings, + + patches: [ + // Force home deprecation override + { + find: "GuildFeatures.GUILD_HOME_DEPRECATION_OVERRIDE", + all: true, + replacement: [ + { + match: /\i\.hasFeature\(\i\.GuildFeatures\.GUILD_HOME_DEPRECATION_OVERRIDE\)/g, + replace: "true" + } + ], + }, + // Disable feedback prompts + { + find: "GuildHomeFeedbackExperiment.definition.id", + replacement: [ + { + match: /return{showFeedback:\i,setOnDismissedFeedback:(\i)}/, + replace: "return{showFeedback:false,setOnDismissedFeedback:$1}" + } + ] + }, + // This feature was never finished, so the patch is disabled + + // Enable guild feed render mode selector + // { + // find: "2022-01_home_feed_toggle", + // replacement: [ + // { + // match: /showSelector:!1/, + // replace: "showSelector:true" + // } + // ] + // }, + + // Fix focusMessage clearing previously cached messages and causing a loop when fetching messages around home messages + { + find: '"MessageActionCreators"', + replacement: { + match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/, + replace: "before:$1," + } + }, + // Force Server Home instead of Server Guide + { + find: "61eef9_2", + replacement: { + match: /(?<=getMutableGuildChannelsForGuild\(\i\)\);)(?=if\(null==\i\|\|)/, + replace: "if($self.useForceServerHome())return false;" + } + } + ], + + start() { + addContextMenuPatch("guild-context", contextMenuPatch); + }, + + stop() { + removeContextMenuPatch("guild-context", contextMenuPatch); + }, + + useForceServerHome() { + const { forceServerHome } = settings.use(["forceServerHome"]); + + return forceServerHome; + } +}); From 612fdf8952c8b3ad80e16552a146088deadecb53 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 6 Mar 2024 07:11:14 -0300 Subject: [PATCH 095/356] FakeNitro: Fix trimming wrong content --- src/plugins/fakeNitro/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 8cf2492a8..68560817c 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -585,13 +585,15 @@ export default definePlugin({ for (const [index, child] of children.entries()) children[index] = modifyChild(child); children = this.clearEmptyArrayItems(children); - this.trimContent(children); return children; }; try { - return modifyChildren(lodash.cloneDeep(content)); + const newContent = modifyChildren(lodash.cloneDeep(content)); + this.trimContent(newContent); + + return newContent; } catch (err) { new Logger("FakeNitro").error(err); return content; From 42a9fa2d47d2e0d0e4233436086e93f0b3dcb1af Mon Sep 17 00:00:00 2001 From: Kyuuhachi <1547062+Kyuuhachi@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:06:24 +0100 Subject: [PATCH 096/356] Refactor ContextMenuAPI (#2236) --- src/api/ContextMenu.ts | 53 ++++++++++++++++------- src/plugins/_api/contextMenu.ts | 6 +-- src/plugins/_core/settings.tsx | 12 ++--- src/plugins/biggerStreamPreview/index.tsx | 14 +++--- src/plugins/copyUserURLs/index.tsx | 15 +++---- src/plugins/emoteCloner/index.tsx | 18 +++----- src/plugins/imageZoom/index.tsx | 23 +++++----- src/plugins/index.ts | 17 +++++++- src/plugins/messageLogger/index.tsx | 13 +++--- src/plugins/permissionsViewer/index.tsx | 27 ++++-------- src/plugins/pinDms/contextMenus.tsx | 19 +++----- src/plugins/pinDms/index.tsx | 6 +-- src/plugins/resurrectHome/index.tsx | 52 ++++++++++------------ src/plugins/reverseImageSearch/index.tsx | 18 +++----- src/plugins/reviewDB/index.tsx | 13 +++--- src/plugins/searchReply/index.tsx | 19 ++++---- src/plugins/serverProfile/index.tsx | 14 +++--- src/plugins/translate/index.tsx | 9 ++-- src/plugins/unsuppressEmbeds/index.tsx | 15 +++---- src/plugins/vencordToolbox/index.tsx | 9 ++-- src/plugins/viewIcons/index.tsx | 17 +++----- src/plugins/viewRaw/index.tsx | 22 ++++------ src/plugins/voiceMessages/index.tsx | 45 +++++++++---------- src/utils/constants.ts | 4 ++ src/utils/types.ts | 5 +++ 25 files changed, 220 insertions(+), 245 deletions(-) diff --git a/src/api/ContextMenu.ts b/src/api/ContextMenu.ts index d66d98c4f..fdd4facf4 100644 --- a/src/api/ContextMenu.ts +++ b/src/api/ContextMenu.ts @@ -17,22 +17,20 @@ */ import { Logger } from "@utils/Logger"; +import { Menu, React } from "@webpack/common"; import type { ReactElement } from "react"; -type ContextMenuPatchCallbackReturn = (() => void) | void; /** * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example - * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; +export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => void; /** * @param navId The navId of the context menu being patched * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example - * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; +export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => void; const ContextMenuLogger = new Logger("ContextMenu"); @@ -93,14 +91,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba * @param id The id of the child. If an array is specified, all ids will be tried * @param children The context menu children */ -export function findGroupChildrenByChildId(id: string | string[], children: Array, _itemsArray?: Array): Array | null { +export function findGroupChildrenByChildId(id: string | string[], children: Array): Array | null { for (const child of children) { if (child == null) continue; + if (Array.isArray(child)) { + const found = findGroupChildrenByChildId(id, child); + if (found !== null) return found; + } + if ( (Array.isArray(id) && id.some(id => child.props?.id === id)) || child.props?.id === id - ) return _itemsArray ?? null; + ) return children; let nextChildren = child.props?.children; if (nextChildren) { @@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra child.props.children = nextChildren; } - const found = findGroupChildrenByChildId(id, nextChildren, nextChildren); + const found = findGroupChildrenByChildId(id, nextChildren); if (found !== null) return found; } } @@ -126,9 +129,12 @@ interface ContextMenuProps { onClose: (callback: (...args: Array) => any) => void; } -const patchedMenus = new WeakSet(); +export function _usePatchContextMenu(props: ContextMenuProps) { + props = { + ...props, + children: cloneMenuChildren(props.children), + }; -export function _patchContextMenu(props: ContextMenuProps) { props.contextMenuApiArguments ??= []; const contextMenuPatches = navPatches.get(props.navId); @@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) { if (contextMenuPatches) { for (const patch of contextMenuPatches) { try { - const callback = patch(props.children, ...props.contextMenuApiArguments); - if (!patchedMenus.has(props)) callback?.(); + patch(props.children, ...props.contextMenuApiArguments); } catch (err) { ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); } @@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) { for (const patch of globalPatches) { try { - const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments); - if (!patchedMenus.has(props)) callback?.(); + patch(props.navId, props.children, ...props.contextMenuApiArguments); } catch (err) { ContextMenuLogger.error("Global patch errored,", err); } } - patchedMenus.add(props); + return props; +} + +function cloneMenuChildren(obj: ReactElement | Array | null) { + if (Array.isArray(obj)) { + return obj.map(cloneMenuChildren); + } + + if (React.isValidElement(obj)) { + obj = React.cloneElement(obj); + + if ( + obj?.props?.children && + (obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null) + ) { + obj.props.children = cloneMenuChildren(obj.props.children); + } + } + + return obj; } diff --git a/src/plugins/_api/contextMenu.ts b/src/plugins/_api/contextMenu.ts index 55fdf3eae..01619546d 100644 --- a/src/plugins/_api/contextMenu.ts +++ b/src/plugins/_api/contextMenu.ts @@ -22,15 +22,15 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "ContextMenuAPI", description: "API for adding/removing items to/from context menus.", - authors: [Devs.Nuckyz, Devs.Ven], + authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi], required: true, patches: [ { find: "♫ (つ。◕‿‿◕。)つ ♪", replacement: { - match: /let{navId:/, - replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&" + match: /(?=let{navId:)(?<=function \i\((\i)\).+?)/, + replace: "$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);" } }, { diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 6f43b76a8..01220eb4e 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -30,21 +30,21 @@ export default definePlugin({ authors: [Devs.Ven, Devs.Megu], required: true, - start() { + contextMenus: { // The settings shortcuts in the user settings cog context menu // read the elements from a hardcoded map which for obvious reason // doesn't contain our sections. This patches the actions of our // sections to manually use SettingsRouter (which only works on desktop // but the context menu is usually not available on mobile anyway) - addContextMenuPatch("user-settings-cog", children => () => { - const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any; + "user-settings-cog"(children) { + const section = findGroupChildrenByChildId("VencordSettings", children); section?.forEach(c => { const id = c?.props?.id; if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) { - c.props.action = () => SettingsRouter.open(id); + c!.props.action = () => SettingsRouter.open(id); } }); - }); + } }, patches: [{ diff --git a/src/plugins/biggerStreamPreview/index.tsx b/src/plugins/biggerStreamPreview/index.tsx index 40bbe30a8..8cca912bc 100644 --- a/src/plugins/biggerStreamPreview/index.tsx +++ b/src/plugins/biggerStreamPreview/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { ScreenshareIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { openImageModal } from "@utils/discord"; @@ -60,7 +60,7 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica openImageModal(previewUrl); }; -export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => () => { +export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => { const stream = ApplicationStreamingStore.getAnyStreamForUser(userId); if (!stream) return; @@ -89,12 +89,8 @@ 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); + contextMenus: { + "user-context": userContextPatch, + "stream-context": streamContextPatch } }); diff --git a/src/plugins/copyUserURLs/index.tsx b/src/plugins/copyUserURLs/index.tsx index 9f69674cf..7af8502db 100644 --- a/src/plugins/copyUserURLs/index.tsx +++ b/src/plugins/copyUserURLs/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { LinkIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -29,7 +29,7 @@ interface UserContextProps { user: User; } -const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => { +const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => { if (!user) return; children.push( @@ -46,12 +46,7 @@ export default definePlugin({ name: "CopyUserURLs", authors: [Devs.castdrian], description: "Adds a 'Copy User URL' option to the user context menu.", - - start() { - addContextMenuPatch("user-context", UserContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("user-context", UserContextMenuPatch); - }, + contextMenus: { + "user-context": UserContextMenuPatch + } }); diff --git a/src/plugins/emoteCloner/index.tsx b/src/plugins/emoteCloner/index.tsx index 219ce435f..b25e1be27 100644 --- a/src/plugins/emoteCloner/index.tsx +++ b/src/plugins/emoteCloner/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; @@ -312,7 +312,7 @@ function isGifUrl(url: string) { return new URL(url).pathname.endsWith(".gif"); } -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {}; if (!favoriteableId) return; @@ -341,7 +341,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) = findGroupChildrenByChildId("copy-link", children)?.push(menuItem); }; -const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => { +const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => { const { id, name, type } = props?.target?.dataset ?? {}; if (!id) return; @@ -363,14 +363,8 @@ export default definePlugin({ description: "Allows you to clone Emotes & Stickers to your own server (right click them)", tags: ["StickerCloner"], authors: [Devs.Ven, Devs.Nuckyz], - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - addContextMenuPatch("expression-picker", expressionPickerPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - removeContextMenuPatch("expression-picker", expressionPickerPatch); + contextMenus: { + "message": messageContextMenuPatch, + "expression-picker": expressionPickerPatch } }); diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 8d8b6726d..048c0ed5b 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -16,14 +16,14 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; import { debounce } from "@utils/debounce"; import definePlugin, { OptionType } from "@utils/types"; -import { ContextMenuApi, Menu, React, ReactDOM } from "@webpack/common"; +import { Menu, React, ReactDOM } from "@webpack/common"; import type { Root } from "react-dom/client"; import { Magnifier, MagnifierProps } from "./components/Magnifier"; @@ -80,25 +80,25 @@ export const settings = definePluginSettings({ }); -const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => { +const imageContextMenuPatch: NavContextMenuPatchCallback = children => { + const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]); + children.push( { - settings.store.square = !settings.store.square; - ContextMenuApi.closeContextMenu(); + settings.store.square = !square; }} /> { - settings.store.nearestNeighbour = !settings.store.nearestNeighbour; - ContextMenuApi.closeContextMenu(); + settings.store.nearestNeighbour = !nearestNeighbour; }} /> | null, @@ -245,7 +248,6 @@ export default definePlugin({ start() { enableStyle(styles); - addContextMenuPatch("image-context", imageContextMenuPatch); this.element = document.createElement("div"); this.element.classList.add("MagnifierContainer"); document.body.appendChild(this.element); @@ -256,6 +258,5 @@ export default definePlugin({ // so componenetWillUnMount gets called if Magnifier component is still alive this.root && this.root.unmount(); this.element?.remove(); - removeContextMenuPatch("image-context", imageContextMenuPatch); } }); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 234838606..7092001ee 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -17,6 +17,7 @@ */ import { registerCommand, unregisterCommand } from "@api/Commands"; +import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; import { Patch, Plugin, StartAt } from "@utils/types"; @@ -119,7 +120,7 @@ export function startDependenciesRecursive(p: Plugin) { } export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { - const { name, commands, flux } = p; + const { name, commands, flux, contextMenus } = p; if (p.start) { logger.info("Starting plugin", name); @@ -154,11 +155,17 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } } + if (contextMenus) { + for (const navId in contextMenus) { + addContextMenuPatch(navId, contextMenus[navId]); + } + } + return true; }, p => `startPlugin ${p.name}`); export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) { - const { name, commands, flux } = p; + const { name, commands, flux, contextMenus } = p; if (p.stop) { logger.info("Stopping plugin", name); if (!p.started) { @@ -192,5 +199,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } } + if (contextMenus) { + for (const navId in contextMenus) { + removeContextMenuPatch(navId, contextMenus[navId]); + } + } + return true; }, p => `stopPlugin ${p.name}`); diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index ef986bf87..8bc563b19 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -18,7 +18,7 @@ import "./messageLogger.css"; -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -45,7 +45,7 @@ function addDeleteStyle() { const REMOVE_HISTORY_ID = "ml-remove-history"; const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style"; -const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => { +const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => { const { message } = props; const { deleted, editHistory, id, channel_id } = message; @@ -94,13 +94,12 @@ export default definePlugin({ description: "Temporarily logs deleted and edited messages.", authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN], - start() { - addDeleteStyle(); - addContextMenuPatch("message", patchMessageContextMenu); + contextMenus: { + "message": patchMessageContextMenu }, - stop() { - removeContextMenuPatch("message", patchMessageContextMenu); + start() { + addDeleteStyle(); }, renderEdit(edit: { timestamp: any, content: string; }) { diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 9e0131e64..07f073d63 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -18,7 +18,7 @@ import "./styles.css"; -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -125,10 +125,10 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { } function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback { - return (children, props) => () => { + return (children, props) => { if (!props) return; if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild))) - return children; + return; const group = findGroupChildrenByChildId(childId, children); @@ -173,19 +173,10 @@ export default definePlugin({ UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && , - userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User), - channelContextMenuPatch: makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel), - guildContextMenuPatch: makeContextMenuPatch("privacy", MenuItemParentType.Guild), - - start() { - addContextMenuPatch("user-context", this.userContextMenuPatch); - addContextMenuPatch("channel-context", this.channelContextMenuPatch); - addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("user-context", this.userContextMenuPatch); - removeContextMenuPatch("channel-context", this.channelContextMenuPatch); - removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch); - }, + contextMenus: { + "user-context": makeContextMenuPatch("roles", MenuItemParentType.User), + "channel-context": makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel), + "guild-context": makeContextMenuPatch("privacy", MenuItemParentType.Guild), + "guild-header-popout": makeContextMenuPatch("privacy", MenuItemParentType.Guild) + } }); diff --git a/src/plugins/pinDms/contextMenus.tsx b/src/plugins/pinDms/contextMenus.tsx index 7d89ec123..1db8b25a9 100644 --- a/src/plugins/pinDms/contextMenus.tsx +++ b/src/plugins/pinDms/contextMenus.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Menu } from "@webpack/common"; import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings"; @@ -50,13 +50,13 @@ function PinMenuItem(channelId: string) { ); } -const GroupDMContext: NavContextMenuPatchCallback = (children, props) => () => { +const GroupDMContext: NavContextMenuPatchCallback = (children, props) => { const container = findGroupChildrenByChildId("leave-channel", children); if (container) container.unshift(PinMenuItem(props.channel.id)); }; -const UserContext: NavContextMenuPatchCallback = (children, props) => () => { +const UserContext: NavContextMenuPatchCallback = (children, props) => { const container = findGroupChildrenByChildId("close-dm", children); if (container) { const idx = container.findIndex(c => c?.props?.id === "close-dm"); @@ -64,12 +64,7 @@ const UserContext: NavContextMenuPatchCallback = (children, props) => () => { } }; -export function addContextMenus() { - addContextMenuPatch("gdm-context", GroupDMContext); - addContextMenuPatch("user-context", UserContext); -} - -export function removeContextMenus() { - removeContextMenuPatch("gdm-context", GroupDMContext); - removeContextMenuPatch("user-context", UserContext); -} +export const contextMenus = { + "gdm-context": GroupDMContext, + "user-context": UserContext +}; diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 792bdab6a..943f0f1b1 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -20,7 +20,7 @@ import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { Channel } from "discord-types/general"; -import { addContextMenus, removeContextMenus } from "./contextMenus"; +import { contextMenus } from "./contextMenus"; import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings"; export default definePlugin({ @@ -29,9 +29,7 @@ export default definePlugin({ authors: [Devs.Ven, Devs.Strencher], settings, - - start: addContextMenus, - stop: removeContextMenus, + contextMenus, usePinCount(channelIds: string[]) { const pinnedDms = usePinnedDms(); diff --git a/src/plugins/resurrectHome/index.tsx b/src/plugins/resurrectHome/index.tsx index 91ed87a02..6b0069a7f 100644 --- a/src/plugins/resurrectHome/index.tsx +++ b/src/plugins/resurrectHome/index.tsx @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { ContextMenuApi, Menu } from "@webpack/common"; +import { Menu } from "@webpack/common"; const settings = definePluginSettings({ forceServerHome: { @@ -30,25 +30,11 @@ const settings = definePluginSettings({ } }); -const contextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (!props?.guild) return; +function useForceServerHome() { + const { forceServerHome } = settings.use(["forceServerHome"]); - const group = findGroupChildrenByChildId("hide-muted-channels", children); - - group?.unshift( - { - settings.store.forceServerHome = !settings.store.forceServerHome; - ContextMenuApi.closeContextMenu(); - }} - /> - - ); -}; + return forceServerHome; +} export default definePlugin({ name: "ResurrectHome", @@ -109,17 +95,25 @@ export default definePlugin({ } ], - start() { - addContextMenuPatch("guild-context", contextMenuPatch); - }, + useForceServerHome, - stop() { - removeContextMenuPatch("guild-context", contextMenuPatch); - }, + contextMenus: { + "guild-context"(children, props) { + const forceServerHome = useForceServerHome(); - useForceServerHome() { - const { forceServerHome } = settings.use(["forceServerHome"]); + if (!props?.guild) return; - return forceServerHome; + const group = findGroupChildrenByChildId("hide-muted-channels", children); + + group?.unshift( + settings.store.forceServerHome = !forceServerHome} + /> + ); + } } }); diff --git a/src/plugins/reverseImageSearch/index.tsx b/src/plugins/reverseImageSearch/index.tsx index 6c5f3e729..415dc13d8 100644 --- a/src/plugins/reverseImageSearch/index.tsx +++ b/src/plugins/reverseImageSearch/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Flex } from "@components/Flex"; import { OpenExternalIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; @@ -84,7 +84,7 @@ function makeSearchItem(src: string) { ); } -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { if (props?.reverseImageSearchType !== "img") return; const src = props.itemHref ?? props.itemSrc; @@ -93,7 +93,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) = group?.push(makeSearchItem(src)); }; -const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { +const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { if (!props?.src) return; const group = findGroupChildrenByChildId("copy-native-link", children) ?? children; @@ -115,14 +115,8 @@ export default definePlugin({ } } ], - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - addContextMenuPatch("image-context", imageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - removeContextMenuPatch("image-context", imageContextMenuPatch); + contextMenus: { + "message": messageContextMenuPatch, + "image-context": imageContextMenuPatch } }); diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index 50bb62184..ad24e9696 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -18,7 +18,7 @@ import "./style.css"; -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import ErrorBoundary from "@components/ErrorBoundary"; import ExpandableHeader from "@components/ExpandableHeader"; import { OpenExternalIcon } from "@components/Icons"; @@ -36,7 +36,7 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { settings } from "./settings"; import { showToast } from "./utils"; -const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => { +const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => { children.push( { const [reviewCount, setReviewCount] = useState(); diff --git a/src/plugins/searchReply/index.tsx b/src/plugins/searchReply/index.tsx index b151712af..35b197874 100644 --- a/src/plugins/searchReply/index.tsx +++ b/src/plugins/searchReply/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { ReplyIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -27,7 +27,7 @@ import { Message } from "discord-types/general"; const messageUtils = findByPropsLazy("replyToMessage"); -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => { // make sure the message is in the selected channel if (SelectedChannelStore.getChannelId() !== message.channel_id) return; const channel = ChannelStore.getChannel(message?.channel_id); @@ -38,7 +38,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag const dmGroup = findGroupChildrenByChildId("pin", children); if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) { const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin"); - return dmGroup.splice(pinIndex + 1, 0, ( + dmGroup.splice(pinIndex + 1, 0, ( messageUtils.replyToMessage(channel, message, e)} /> )); + return; } // servers const serverGroup = findGroupChildrenByChildId("mark-unread", children); if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) { - return serverGroup.unshift(( + serverGroup.unshift(( messageUtils.replyToMessage(channel, message, e)} /> )); + return; } }; @@ -67,12 +69,7 @@ export default definePlugin({ name: "SearchReply", description: "Adds a reply button to search results", authors: [Devs.Aria], - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); + contextMenus: { + "message": messageContextMenuPatch } }); diff --git a/src/plugins/serverProfile/index.tsx b/src/plugins/serverProfile/index.tsx index 68f6193cc..9d495c9d3 100644 --- a/src/plugins/serverProfile/index.tsx +++ b/src/plugins/serverProfile/index.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { Menu } from "@webpack/common"; @@ -12,7 +12,7 @@ import { Guild } from "discord-types/general"; import { openGuildProfileModal } from "./GuildProfileModal"; -const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => { +const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => { const group = findGroupChildrenByChildId("privacy", children); group?.push( @@ -29,12 +29,8 @@ export default definePlugin({ description: "Allows you to view info about a server by right clicking it in the server list", authors: [Devs.Ven, Devs.Nuckyz], tags: ["guild", "info"], - - start() { - addContextMenuPatch(["guild-context", "guild-header-popout"], Patch); - }, - - stop() { - removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch); + contextMenus: { + "guild-context": Patch, + "guild-header-popout": Patch } }); diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index 702e60cf7..f602d1255 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -19,7 +19,7 @@ import "./styles.css"; import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { addButton, removeButton } from "@api/MessagePopover"; @@ -32,7 +32,7 @@ import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon"; import { handleTranslate, TranslationAccessory } from "./TranslationAccessory"; import { translate } from "./utils"; -const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => () => { +const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => { if (!message.content) return; const group = findGroupChildrenByChildId("copy-text", children); @@ -57,13 +57,15 @@ export default definePlugin({ authors: [Devs.Ven], dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], settings, + contextMenus: { + "message": messageCtxPatch + }, // not used, just here in case some other plugin wants it or w/e translate, start() { addAccessory("vc-translation", props => ); - addContextMenuPatch("message", messageCtxPatch); addChatBarButton("vc-translate", TranslateChatBarIcon); addButton("vc-translate", message => { @@ -91,7 +93,6 @@ export default definePlugin({ stop() { removePreSendListener(this.preSend); - removeContextMenuPatch("message", messageCtxPatch); removeChatBarButton("vc-translate"); removeButton("vc-translate"); removeAccessory("vc-translation"); diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx index a21960774..0e87201c6 100644 --- a/src/plugins/unsuppressEmbeds/index.tsx +++ b/src/plugins/unsuppressEmbeds/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { ImageInvisible, ImageVisible } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -24,7 +24,7 @@ import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@web const EMBED_SUPPRESSED = 1 << 2; -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => { const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0; if (!isEmbedSuppressed && !embeds.length) return; @@ -56,12 +56,7 @@ 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); - }, + contextMenus: { + "message": messageContextMenuPatch + } }); diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index 0a805a0d2..ba295fa72 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -19,7 +19,7 @@ import "./index.css"; import { openNotificationLogModal } from "@api/Notifications/notificationLog"; -import { Settings } from "@api/Settings"; +import { Settings, useSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -30,6 +30,8 @@ import type { ReactNode } from "react"; const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider"); function VencordPopout(onClose: () => void) { + const { useQuickCss } = useSettings(); + const pluginEntries = [] as ReactNode[]; for (const plugin of Object.values(Vencord.Plugins.plugins)) { @@ -68,11 +70,10 @@ function VencordPopout(onClose: () => void) { /> { - Settings.useQuickCss = !Settings.useQuickCss; - onClose(); + Settings.useQuickCss = !useQuickCss; }} /> . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { ImageIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; @@ -80,7 +80,7 @@ function openImage(url: string) { }); } -const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => { +const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => { if (!user) return; const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null; @@ -109,7 +109,7 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U )); }; -const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => { +const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => { if (!guild) return; const { id, icon, banner } = guild; @@ -155,14 +155,9 @@ export default definePlugin({ openImage, - start() { - addContextMenuPatch("user-context", UserContext); - addContextMenuPatch("guild-context", GuildContext); - }, - - stop() { - removeContextMenuPatch("user-context", UserContext); - removeContextMenuPatch("guild-context", GuildContext); + contextMenus: { + "user-context": UserContext, + "guild-context": GuildContext }, patches: [ diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 08acdc4c5..68b33eed0 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import { CodeBlock } from "@components/CodeBlock"; @@ -117,8 +117,8 @@ const settings = definePluginSettings({ } }); -function MakeContextCallback(name: "Guild" | "User" | "Channel") { - const callback: NavContextMenuPatchCallback = (children, props) => () => { +function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback { + return (children, props) => { const value = props[name.toLowerCase()]; if (!value) return; if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings @@ -141,16 +141,19 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel") { /> ); }; - return callback; } - export default definePlugin({ name: "ViewRaw", description: "Copy and view the raw content/data of any message, channel or guild", authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna], dependencies: ["MessagePopoverAPI"], settings, + contextMenus: { + "guild-context": MakeContextCallback("Guild"), + "channel-context": MakeContextCallback("Channel"), + "user-context": MakeContextCallback("User") + }, start() { addButton("ViewRaw", msg => { @@ -187,16 +190,9 @@ export default definePlugin({ onContextMenu: handleContextMenu }; }); - - addContextMenuPatch("guild-context", MakeContextCallback("Guild")); - addContextMenuPatch("channel-context", MakeContextCallback("Channel")); - addContextMenuPatch("user-context", MakeContextCallback("User")); }, stop() { - removeButton("CopyRawMessage"); - removeContextMenuPatch("guild-context", MakeContextCallback("Guild")); - removeContextMenuPatch("channel-context", MakeContextCallback("Channel")); - removeContextMenuPatch("user-context", MakeContextCallback("User")); + removeButton("ViewRaw"); } }); diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index 2393ef2b6..2f232f341 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -18,7 +18,7 @@ import "./styles.css"; -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Microphone } from "@components/Icons"; import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; @@ -48,18 +48,30 @@ export type VoiceRecorder = ComponentType<{ const VoiceRecorder = IS_DISCORD_DESKTOP ? VoiceRecorderDesktop : VoiceRecorderWeb; +const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => { + if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return; + + children.push( + + +
Send voice message
+
+ } + action={() => openModal(modalProps => )} + /> + ); +}; + export default definePlugin({ name: "VoiceMessages", description: "Allows you to send voice messages like on mobile. To do so, right click the upload button and click Send Voice Message", authors: [Devs.Ven, Devs.Vap, Devs.Nickyux], settings, - - start() { - addContextMenuPatch("channel-attach", ctxMenuPatch); - }, - - stop() { - removeContextMenuPatch("channel-attach", ctxMenuPatch); + contextMenus: { + "channel-attach": ctxMenuPatch } }); @@ -234,20 +246,3 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { ); } - -const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return; - - children.push( - - -
Send voice message
-
- } - action={() => openModal(modalProps => )} - /> - ); -}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d66bdc826..d213ce272 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -418,6 +418,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ Av32000: { name: "Av32000", id: 593436735380127770n, + }, + Kyuuhachi: { + name: "Kyuuhachi", + id: 236588665420251137n, } } satisfies Record); diff --git a/src/utils/types.ts b/src/utils/types.ts index 16867a43c..bec7cb0b3 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -17,6 +17,7 @@ */ import { Command } from "@api/Commands"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { FluxEvents } from "@webpack/types"; import { Promisable } from "type-fest"; @@ -115,6 +116,10 @@ export interface PluginDef { flux?: { [E in FluxEvents]?: (event: any) => void; }; + /** + * Allows you to manipulate context menus + */ + contextMenus?: Record; /** * Allows you to add custom actions to the Vencord Toolbox. * The key will be used as text for the button From 980206d31568e479f0f0c7a32ab24035374f06d5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 7 Mar 2024 09:36:59 -0300 Subject: [PATCH 097/356] Fix waitFor initial finds traces getting logged to the console even though they always fail --- src/webpack/webpack.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index a68890a83..0f7d8b73c 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -475,8 +475,10 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback else if (typeof filter !== "function") throw new Error("filter must be a string, string[] or function, got " + typeof filter); - const [existing, id] = find(filter!, { isIndirect: true, isWaitFor: true }); - if (existing) return void callback(existing, id); + if (cache != null) { + const [existing, id] = find(filter, { isIndirect: true, isWaitFor: true }); + if (existing) return void callback(existing, id); + } subscriptions.set(filter, callback); } From a59c14f9aa8034fcb3df45af855a7a57c2882b82 Mon Sep 17 00:00:00 2001 From: AutumnVN Date: Thu, 7 Mar 2024 19:51:14 +0700 Subject: [PATCH 098/356] CustomRPC: Change timestamp to milisecond (#2231) --- src/plugins/customRPC/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx index 3653a0776..e70f8c908 100644 --- a/src/plugins/customRPC/index.tsx +++ b/src/plugins/customRPC/index.tsx @@ -175,7 +175,7 @@ const settings = definePluginSettings({ }, startTime: { type: OptionType.NUMBER, - description: "Start timestamp (only for custom timestamp mode)", + description: "Start timestamp in milisecond (only for custom timestamp mode)", onChange: onChange, disabled: isTimestampDisabled, isValid: (value: number) => { @@ -185,7 +185,7 @@ const settings = definePluginSettings({ }, endTime: { type: OptionType.NUMBER, - description: "End timestamp (only for custom timestamp mode)", + description: "End timestamp in milisecond (only for custom timestamp mode)", onChange: onChange, disabled: isTimestampDisabled, isValid: (value: number) => { @@ -313,12 +313,12 @@ async function createActivity(): Promise { switch (settings.store.timestampMode) { case TimestampMode.NOW: activity.timestamps = { - start: Math.floor(Date.now() / 1000) + start: Date.now() }; break; case TimestampMode.TIME: activity.timestamps = { - start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds() + start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000 }; break; case TimestampMode.CUSTOM: From f70114238c1307e086db537640926a0db78adb10 Mon Sep 17 00:00:00 2001 From: Sam <149597648+cheesesamwich@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:55:51 +0000 Subject: [PATCH 099/356] MemberCount: Add options to choose where the member count will be displayed (#2224) --- src/plugins/memberCount/index.tsx | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index eb4ce372c..92e9a2057 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -18,10 +18,11 @@ import "./style.css"; +import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { findStoreLazy } from "@webpack"; import { FluxStore } from "@webpack/types"; @@ -32,6 +33,21 @@ export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxSto getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; }; }; +const settings = definePluginSettings({ + toolTip: { + type: OptionType.BOOLEAN, + description: "If the member count should be displayed on the server tooltip", + default: true, + restartNeeded: true + }, + memberList: { + type: OptionType.BOOLEAN, + description: "If the member count should be displayed on the member list", + default: true, + restartNeeded: true + } +}); + const sharedIntlNumberFormat = new Intl.NumberFormat(); export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value); export const cl = classNameFactory("vc-membercount-"); @@ -40,6 +56,7 @@ export default definePlugin({ name: "MemberCount", description: "Shows the amount of online & total members in the server member list and tooltip", authors: [Devs.Ven, Devs.Commandtechno], + settings, patches: [ { @@ -47,17 +64,18 @@ export default definePlugin({ replacement: { match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, replace: ":[$1?.startsWith('members')?$self.render():null,$2" - } + }, + predicate: () => settings.store.memberList }, { find: ".invitesDisabledTooltip", replacement: { match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/, replace: ",$self.renderTooltip(arguments[0].guild)]" - } + }, + predicate: () => settings.store.toolTip } ], - render: ErrorBoundary.wrap(MemberCount, { noop: true }), renderTooltip: ErrorBoundary.wrap(guild => , { noop: true }) }); From 19799767adef192a8451adf0f8c1710933b21a41 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:09:04 -0300 Subject: [PATCH 100/356] Fix trying to check for updates if origin doesn't have same branch --- src/main/updater/git.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts index 2ff3ba512..20a5d7003 100644 --- a/src/main/updater/git.ts +++ b/src/main/updater/git.ts @@ -49,9 +49,12 @@ async function getRepo() { async function calculateGitChanges() { await git("fetch"); - const branch = await git("branch", "--show-current"); + const branch = (await git("branch", "--show-current")).stdout.trim(); - const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s"); + const existsOnOrigin = (await git("ls-remote", "origin", branch)).stdout.length > 0; + if (!existsOnOrigin) return []; + + const res = await git("log", `HEAD...origin/${branch}`, "--pretty=format:%an/%h/%s"); const commits = res.stdout.trim(); return commits ? commits.split("\n").map(line => { From 102842d5288fd22821e2a6eb295255fd88603964 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:33:00 -0300 Subject: [PATCH 101/356] Close Ipc FS watchers if window is closed --- src/main/ipcMain.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 47d400eb6..3ac8a14c5 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -23,7 +23,7 @@ import { debounce } from "@utils/debounce"; import { IpcEvents } from "@utils/IpcEvents"; import { Queue } from "@utils/Queue"; import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; -import { mkdirSync, readFileSync, watch } from "fs"; +import { FSWatcher, mkdirSync, readFileSync, watch } from "fs"; import { open, readdir, readFile, writeFile } from "fs/promises"; import { join, normalize } from "path"; @@ -126,16 +126,23 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => { export function initIpc(mainWindow: BrowserWindow) { + let quickCssWatcher: FSWatcher | undefined; + open(QUICKCSS_PATH, "a+").then(fd => { fd.close(); - watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { + quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); - }); + }).catch(() => { }); - watch(THEMES_DIR, { persistent: false }, debounce(() => { + const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => { mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0); })); + + mainWindow.once("closed", () => { + quickCssWatcher?.close(); + themesWatcher.close(); + }); } ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { From 1c1d82f9a820ce4b7611032488624d98624a7ecb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:06:08 -0300 Subject: [PATCH 102/356] VencordToolbox: don't subscribe to all settings Also remove one indirection from useSettings --- src/api/Settings.ts | 14 ++++++++------ src/plugins/vencordToolbox/index.tsx | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 004a8988b..c1ff6915b 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -223,13 +223,13 @@ export const Settings = makeProxy(settings); export function useSettings(paths?: UseSettings[]) { const [, forceUpdate] = React.useReducer(() => ({}), {}); - const onUpdate: SubscriptionCallback = paths - ? (value, path) => paths.includes(path as UseSettings) && forceUpdate() - : forceUpdate; + if (paths) { + (forceUpdate as SubscriptionCallback)._paths = paths; + } React.useEffect(() => { - subscriptions.add(onUpdate); - return () => void subscriptions.delete(onUpdate); + subscriptions.add(forceUpdate); + return () => void subscriptions.delete(forceUpdate); }, []); return Settings; @@ -253,8 +253,10 @@ type ResolvePropDeep = P extends "" ? T : export function addSettingsListener(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void; export function addSettingsListener(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep, path: Path extends "" ? string : Path) => void): void; export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) { - if (path) + if (path) { ((onUpdate as SubscriptionCallback)._paths ??= []).push(path); + } + subscriptions.add(onUpdate); } diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index ba295fa72..00805fbd3 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -30,7 +30,7 @@ import type { ReactNode } from "react"; const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider"); function VencordPopout(onClose: () => void) { - const { useQuickCss } = useSettings(); + const { useQuickCss } = useSettings(["useQuickCss"]); const pluginEntries = [] as ReactNode[]; From 2e90d4c03d98b9e5ea1dd8505cb0740d90b051d0 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 7 Mar 2024 19:53:26 +0100 Subject: [PATCH 103/356] New plugin: BetterRoleContext ~ edit/copy colour shortcuts in profile --- src/plugins/betterRoleContext/README.md | 6 ++ src/plugins/betterRoleContext/index.tsx | 79 ++++++++++++++++++++++ src/webpack/common/settingsStores.ts | 7 +- src/webpack/common/types/index.d.ts | 4 +- src/webpack/common/types/settingsStores.ts | 11 +++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/plugins/betterRoleContext/README.md create mode 100644 src/plugins/betterRoleContext/index.tsx create mode 100644 src/webpack/common/types/settingsStores.ts diff --git a/src/plugins/betterRoleContext/README.md b/src/plugins/betterRoleContext/README.md new file mode 100644 index 000000000..3f3086bdb --- /dev/null +++ b/src/plugins/betterRoleContext/README.md @@ -0,0 +1,6 @@ +# BetterRoleContext + +Adds options to copy role color and edit role when right clicking roles in the user profile + +![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326) + diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx new file mode 100644 index 000000000..7a914293a --- /dev/null +++ b/src/plugins/betterRoleContext/index.tsx @@ -0,0 +1,79 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import { getCurrentGuild } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; + +const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild"); + +function PencilIcon() { + return ( + + + + ); +} + +function AppearanceIcon() { + return ( + + + + ); +} + +export default definePlugin({ + name: "BetterRoleContext", + description: "Adds options to copy role color / edit role when right clicking roles in the user profile", + authors: [Devs.Ven], + + start() { + // DeveloperMode needs to be enabled for the context menu to be shown + TextAndImagesSettingsStores.DeveloperMode.updateSetting(true); + }, + + contextMenus: { + "dev-context"(children, { id }: { id: string; }) { + const guild = getCurrentGuild(); + const role = guild?.roles[id]; + if (!role) return; + + if (role.colorString) { + children.push( + Clipboard.copy(role.colorString!)} + icon={AppearanceIcon} + /> + ); + } + + if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) { + children.push( + { + await GuildSettingsActions.open(guild.id, "ROLES"); + GuildSettingsActions.selectRole(id); + }} + icon={PencilIcon} + /> + ); + } + } + } +}); diff --git a/src/webpack/common/settingsStores.ts b/src/webpack/common/settingsStores.ts index 6db21949a..4a48efda6 100644 --- a/src/webpack/common/settingsStores.ts +++ b/src/webpack/common/settingsStores.ts @@ -6,7 +6,10 @@ import { findByPropsLazy } from "@webpack"; -export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact"); -export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame"); +import * as t from "./types/settingsStores"; + + +export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact") as Record; +export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame") as Record; export const UserSettingsActionCreators = findByPropsLazy("PreloadedUserSettingsActionCreators"); diff --git a/src/webpack/common/types/index.d.ts b/src/webpack/common/types/index.d.ts index af4b5e1fb..01c968553 100644 --- a/src/webpack/common/types/index.d.ts +++ b/src/webpack/common/types/index.d.ts @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +export * from "./classes"; export * from "./components"; export * from "./fluxEvents"; +export * from "./i18nMessages"; export * from "./menu"; +export * from "./settingsStores"; export * from "./stores"; export * from "./utils"; - diff --git a/src/webpack/common/types/settingsStores.ts b/src/webpack/common/types/settingsStores.ts new file mode 100644 index 000000000..5453ca352 --- /dev/null +++ b/src/webpack/common/types/settingsStores.ts @@ -0,0 +1,11 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface SettingsStore { + getSetting(): T; + updateSetting(value: T): void; + useSetting(): T; +} From 688ff255d27de8b5bd611c7e678fac69c7f47032 Mon Sep 17 00:00:00 2001 From: Amia <9750071+aamiaa@users.noreply.github.com> Date: Fri, 8 Mar 2024 04:18:18 +0100 Subject: [PATCH 104/356] fix(MutualGroupDMs): update regex (#2242) --- src/plugins/mutualGroupDMs/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 40d5201cb..f5e4b6149 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -47,8 +47,8 @@ export default definePlugin({ { 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||arguments[0].isCurrentUser)?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),' + match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/, + replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),' } }, { From 10f33b3dec4f2d197f117f780a6c8283f6f1046d Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 8 Mar 2024 00:23:35 -0300 Subject: [PATCH 105/356] Make more finds use filters.componentByCode --- src/plugins/reviewDB/components/ReviewsView.tsx | 6 +++--- src/webpack/common/components.ts | 2 +- src/webpack/webpack.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index eea92bb81..46bd7fb84 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react"; -import { find, findByPropsLazy } from "@webpack"; +import { useAwaiter, useForceUpdater } from "@utils/react"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common"; import { Auth, authorize } from "../auth"; @@ -31,7 +31,7 @@ import ReviewComponent from "./ReviewComponent"; const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms"); const { ChatInputTypes } = findByPropsLazy("ChatInputTypes"); -const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); +const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA"); const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer"); interface UserProps { diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index d7bb5d759..048e65d68 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -51,7 +51,7 @@ export let Avatar: t.Avatar; /** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */ export let useToken: t.useToken; -export const MaskedLink = waitForComponent("MaskedLink", m => m?.type?.toString().includes("MASKED_LINK)")); +export const MaskedLink = waitForComponent("MaskedLink", filters.componentByCode("MASKED_LINK)")); export const Timestamp = waitForComponent("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const Flex = waitForComponent("Flex", ["Justify", "Align", "Wrap"]); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 0f7d8b73c..992bf38f3 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -60,6 +60,7 @@ export const filters = { return m => { if (filter(m)) return true; if (!m.$$typeof) return false; + if (m.type && m.type.render) return filter(m.type.render); // memo + forwardRef if (m.type) return filter(m.type); // memos if (m.render) return filter(m.render); // forwardRefs return false; From cf7830e747a5f2ff0b1bace93a9fab13f6936477 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 8 Mar 2024 00:24:04 -0300 Subject: [PATCH 106/356] ShowHiddenChannels: Fix patches --- src/plugins/showHiddenChannels/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 919f3f3c5..2d091c24a 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -305,27 +305,27 @@ export default definePlugin({ ] }, { - find: ".avatars),children", + find: '+1]})},"overflow"))', replacement: [ { // Create a variable for the channel prop - match: /maxUsers:\i,users:\i.+?=(\i).+?;/, + match: /maxUsers:\i,users:\i.+?}=(\i).*?;/, replace: (m, props) => `${m}let{shcChannel}=${props};` }, { // Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen match: /\i>0(?=&&.{0,60}renderPopout)/, - replace: m => `($self.isHiddenChannel(shcChannel,true)?true:${m})` + replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})` }, { // Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/, - replace: (_, amount) => `($self.isHiddenChannel(shcChannel,true)&&${amount}<=0?0:1)` + replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?0:1)` }, { // Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen match: /(?<="\+",)(\i)\+1/, - replace: (m, amount) => `$self.isHiddenChannel(shcChannel,true)&&${amount}<=0?"":${m}` + replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${m}` } ] }, From b0d37c981e6bbb47b4b25f28d3e87488f9038bac Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 8 Mar 2024 00:29:06 -0300 Subject: [PATCH 107/356] Bump to 1.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dde55d311..dbf8aaf34 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.7.0", + "version": "1.7.1", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 992533245bc08ccaac258aaca803d362942dde74 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 8 Mar 2024 19:05:30 -0300 Subject: [PATCH 108/356] RoleColorEverywhere: Wrap roleGroupColor in ErrorBoundary --- src/plugins/roleColorEverywhere/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index 968027163..14d38e8ee 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -17,6 +17,7 @@ */ import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common"; @@ -112,7 +113,7 @@ export default definePlugin({ return colorString && parseInt(colorString.slice(1), 16); }, - roleGroupColor({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) { + roleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => { const guild = GuildStore.getGuild(guildId); const role = guild?.roles[id]; @@ -125,7 +126,7 @@ export default definePlugin({ {title ?? label} — {count} ); - }, + }, { noop: true }), getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) { return { From 497f0de9a1dc7f228a67e73b25f12c0d6fa93240 Mon Sep 17 00:00:00 2001 From: Jack <30497388+FieryFlames@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:52:11 -0400 Subject: [PATCH 109/356] chore(Decor): Change URL formula for cost savings (#2247) --- src/plugins/decor/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 8cfd8c036..713b17d76 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -131,9 +131,10 @@ export default definePlugin({ getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) { // Only Decor avatar decorations have this SKU ID if (avatarDecoration?.skuId === SKU_ID) { - const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`); - url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString()); - return url.toString(); + const parts = avatarDecoration.asset.split("_"); + // Remove a_ prefix if it's animated and animation is disabled + if (isAnimatedAvatarDecoration(avatarDecoration.asset) && !canAnimate) parts.shift(); + return `${CDN_URL}/${parts.join("_")}.png`; } else if (avatarDecoration?.skuId === RAW_SKU_ID) { return avatarDecoration.asset; } From 34390e03652f523fd92e3974feb93395bb825bec Mon Sep 17 00:00:00 2001 From: Vendicated Date: Mon, 11 Mar 2024 16:13:07 +0100 Subject: [PATCH 110/356] Add workaround for guild role api changes on canary/ptb fixes RoleColorEverywhere, ServerInfo, PermissionViewer, BetterRoleContext --- src/plugins/betterRoleContext/index.tsx | 6 ++++-- .../components/RolesAndUsersPermissions.tsx | 8 +++++--- src/plugins/permissionsViewer/index.tsx | 3 ++- src/plugins/permissionsViewer/utils.ts | 13 ++++++++----- src/plugins/roleColorEverywhere/index.tsx | 6 +++--- src/plugins/serverProfile/GuildProfileModal.tsx | 4 ++-- src/plugins/xsOverlay.desktop/index.ts | 3 ++- src/utils/discord.tsx | 10 +++++++++- 8 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx index 7a914293a..e73779adf 100644 --- a/src/plugins/betterRoleContext/index.tsx +++ b/src/plugins/betterRoleContext/index.tsx @@ -5,7 +5,7 @@ */ import { Devs } from "@utils/constants"; -import { getCurrentGuild } from "@utils/discord"; +import { getCurrentGuild, getGuildRoles } from "@utils/discord"; import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; @@ -47,7 +47,9 @@ export default definePlugin({ contextMenus: { "dev-context"(children, { id }: { id: string; }) { const guild = getCurrentGuild(); - const role = guild?.roles[id]; + if (!guild) return; + + const role = getGuildRoles(guild.id)[id]; if (!role) return; if (role.colorString) { diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx index e0d25c7ab..adadc90b5 100644 --- a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx +++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx @@ -19,7 +19,7 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; -import { getUniqueUsername } from "@utils/discord"; +import { getGuildRoles, getUniqueUsername } from "@utils/discord"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ContextMenuApi, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; import type { Guild } from "discord-types/general"; @@ -78,6 +78,8 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea const [selectedItemIndex, selectItem] = useState(0); const selectedItem = permissions[selectedItemIndex]; + const roles = getGuildRoles(guild.id); + return ( {permissions.map((permission, index) => { const user = UserStore.getUser(permission.id ?? ""); - const role = guild.roles[permission.id ?? ""]; + const role = roles[permission.id ?? ""]; return ( + + + + ); +} + +export const openCategoryModal = (categoryId: string | null, channelId: string | null) => + openModalLazy(async () => { + await requireSettingsMenu(); + return modalProps => ; + }); + diff --git a/src/plugins/pinDms/components/contextMenu.tsx b/src/plugins/pinDms/components/contextMenu.tsx new file mode 100644 index 000000000..0f5a198ba --- /dev/null +++ b/src/plugins/pinDms/components/contextMenu.tsx @@ -0,0 +1,96 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { Menu } from "@webpack/common"; + +import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data"; +import { forceUpdate, settings } from "../index"; +import { openCategoryModal } from "./CreateCategoryModal"; + +function createPinMenuItem(channelId: string) { + const pinned = isPinned(channelId); + + return ( + + + {!pinned && ( + <> + openCategoryModal(null, channelId)} + /> + + + { + categories.map(category => ( + addChannelToCategory(channelId, category.id).then(forceUpdate)} + /> + )) + } + + )} + + {pinned && ( + <> + removeChannelFromCategory(channelId).then(forceUpdate)} + /> + + { + !settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, -1) && ( + moveChannel(channelId, -1).then(forceUpdate)} + /> + ) + } + + { + !settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, 1) && ( + moveChannel(channelId, 1).then(forceUpdate)} + /> + ) + } + + )} + + + ); +} + +const GroupDMContext: NavContextMenuPatchCallback = (children, props) => { + const container = findGroupChildrenByChildId("leave-channel", children); + container?.unshift(createPinMenuItem(props.channel.id)); +}; + +const UserContext: NavContextMenuPatchCallback = (children, props) => { + const container = findGroupChildrenByChildId("close-dm", children); + if (container) { + const idx = container.findIndex(c => c?.props?.id === "close-dm"); + container.splice(idx, 0, createPinMenuItem(props.channel.id)); + } +}; + +export const contextMenus = { + "gdm-context": GroupDMContext, + "user-context": UserContext +}; diff --git a/src/plugins/pinDms/constants.ts b/src/plugins/pinDms/constants.ts new file mode 100644 index 000000000..fbd995e14 --- /dev/null +++ b/src/plugins/pinDms/constants.ts @@ -0,0 +1,32 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export const DEFAULT_CHUNK_SIZE = 256; +export const DEFAULT_COLOR = 10070709; + +export const SWATCHES = [ + 1752220, + 3066993, + 3447003, + 10181046, + 15277667, + 15844367, + 15105570, + 15158332, + 9807270, + 6323595, + + 1146986, + 2067276, + 2123412, + 7419530, + 11342935, + 12745742, + 11027200, + 10038562, + 9936031, + 5533306 +]; diff --git a/src/plugins/pinDms/contextMenus.tsx b/src/plugins/pinDms/contextMenus.tsx deleted file mode 100644 index 1db8b25a9..000000000 --- a/src/plugins/pinDms/contextMenus.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { Menu } from "@webpack/common"; - -import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings"; - -function PinMenuItem(channelId: string) { - const pinned = isPinned(channelId); - const canMove = pinned && settings.store.pinOrder === PinOrder.Custom; - - return ( - <> - togglePin(channelId)} - /> - {canMove && snapshotArray[0] !== channelId && ( - movePin(channelId, -1)} - /> - )} - {canMove && snapshotArray[snapshotArray.length - 1] !== channelId && ( - movePin(channelId, +1)} - /> - )} - - ); -} - -const GroupDMContext: NavContextMenuPatchCallback = (children, props) => { - const container = findGroupChildrenByChildId("leave-channel", children); - if (container) - container.unshift(PinMenuItem(props.channel.id)); -}; - -const UserContext: NavContextMenuPatchCallback = (children, props) => { - const container = findGroupChildrenByChildId("close-dm", children); - if (container) { - const idx = container.findIndex(c => c?.props?.id === "close-dm"); - container.splice(idx, 0, PinMenuItem(props.channel.id)); - } -}; - -export const contextMenus = { - "gdm-context": GroupDMContext, - "user-context": UserContext -}; diff --git a/src/plugins/pinDms/data.ts b/src/plugins/pinDms/data.ts new file mode 100644 index 000000000..afb6f9795 --- /dev/null +++ b/src/plugins/pinDms/data.ts @@ -0,0 +1,214 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import * as DataStore from "@api/DataStore"; +import { Settings } from "@api/Settings"; +import { UserStore } from "@webpack/common"; + +import { DEFAULT_COLOR } from "./constants"; +import { forceUpdate } from "./index"; + +export interface Category { + id: string; + name: string; + color: number; + channels: string[]; + collapsed?: boolean; +} + +const CATEGORY_BASE_KEY = "PinDMsCategories-"; +const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs"; +const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories"; +const OLD_CATEGORY_KEY = "BetterPinDMsCategories-"; + + +export let categories: Category[] = []; + +export async function saveCats(cats: Category[]) { + const { id } = UserStore.getCurrentUser(); + await DataStore.set(CATEGORY_BASE_KEY + id, cats); +} + +export async function init() { + const id = UserStore.getCurrentUser()?.id; + await initCategories(id); + await migrateData(id); + forceUpdate(); +} + +export async function initCategories(userId: string) { + categories = await DataStore.get(CATEGORY_BASE_KEY + userId) ?? []; +} + +export function getCategory(id: string) { + return categories.find(c => c.id === id); +} + +export async function createCategory(category: Category) { + categories.push(category); + await saveCats(categories); +} + +export async function updateCategory(category: Category) { + const index = categories.findIndex(c => c.id === category.id); + if (index === -1) return; + + categories[index] = category; + await saveCats(categories); +} + +export async function addChannelToCategory(channelId: string, categoryId: string) { + const category = categories.find(c => c.id === categoryId); + if (!category) return; + + if (category.channels.includes(channelId)) return; + + category.channels.push(channelId); + await saveCats(categories); + +} + +export async function removeChannelFromCategory(channelId: string) { + const category = categories.find(c => c.channels.includes(channelId)); + if (!category) return; + + category.channels = category.channels.filter(c => c !== channelId); + await saveCats(categories); +} + +export async function removeCategory(categoryId: string) { + const catagory = categories.find(c => c.id === categoryId); + if (!catagory) return; + + // catagory?.channels.forEach(c => removeChannelFromCategory(c)); + categories = categories.filter(c => c.id !== categoryId); + await saveCats(categories); +} + +export async function collapseCategory(id: string, value = true) { + const category = categories.find(c => c.id === id); + if (!category) return; + + category.collapsed = value; + await saveCats(categories); +} + +// utils +export function isPinned(id: string) { + return categories.some(c => c.channels.includes(id)); +} + +export function categoryLen() { + return categories.length; +} + +export function getAllUncollapsedChannels() { + return categories.filter(c => !c.collapsed).map(c => c.channels).flat(); +} + +export function getSections() { + return categories.reduce((acc, category) => { + acc.push(category.channels.length === 0 ? 1 : category.channels.length); + return acc; + }, [] as number[]); +} + +// move categories +export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => { + const a = array[index]; + const b = array[index + direction]; + + return a && b; +}; + +export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => { + const index = categories.findIndex(m => m.id === id); + return canMoveArrayInDirection(categories, index, direction); +}; + +export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1); + +export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => { + const category = categories.find(c => c.channels.includes(channelId)); + if (!category) return false; + + const index = category.channels.indexOf(channelId); + return canMoveArrayInDirection(category.channels, index, direction); +}; + + +function swapElementsInArray(array: any[], index1: number, index2: number) { + if (!array[index1] || !array[index2]) return; + [array[index1], array[index2]] = [array[index2], array[index1]]; +} + +// stolen from PinDMs +export async function moveCategory(id: string, direction: -1 | 1) { + const a = categories.findIndex(m => m.id === id); + const b = a + direction; + + swapElementsInArray(categories, a, b); + + await saveCats(categories); +} + +export async function moveChannel(channelId: string, direction: -1 | 1) { + const category = categories.find(c => c.channels.includes(channelId)); + if (!category) return; + + const a = category.channels.indexOf(channelId); + const b = a + direction; + + swapElementsInArray(category.channels, a, b); + + await saveCats(categories); +} + + + +// migrate data +const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined; + +async function migratePinDMs() { + if (categories.some(m => m.id === "oldPins")) { + return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true); + } + + const pindmspins = getPinDMsPins(); + + // we dont want duplicate pins + const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m))); + if (difference?.length) { + categories.push({ + id: "oldPins", + name: "Pins", + color: DEFAULT_COLOR, + channels: difference + }); + } + + await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true); +} + +async function migrateOldCategories(userId: string) { + const oldCats = await DataStore.get(OLD_CATEGORY_KEY + userId); + // dont want to migrate if the user has already has categories. + if (categories.length === 0 && oldCats?.length) { + categories.push(...(oldCats.filter(m => m.id !== "oldPins"))); + } + await DataStore.set(CATEGORY_MIGRATED_KEY, true); +} + +export async function migrateData(userId: string) { + const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY); + if (m1 && m2) return; + + // want to migrate the old categories first and then slove any conflicts with the PinDMs pins + if (!m1) await migrateOldCategories(userId); + if (!m2) await migratePinDMs(); + + await saveCats(categories); +} diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 45172328e..ee726bfab 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -1,116 +1,131 @@ /* - * 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 . -*/ + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +import "./styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import { classes } from "@utils/misc"; +import definePlugin, { OptionType, StartAt } from "@utils/types"; +import { findByPropsLazy, findStoreLazy } from "@webpack"; +import { ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common"; import { Channel } from "discord-types/general"; -import { contextMenus } from "./contextMenus"; -import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings"; +import { contextMenus } from "./components/contextMenu"; +import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal"; +import { DEFAULT_CHUNK_SIZE } from "./constants"; +import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data"; + +interface ChannelComponentProps { + children: React.ReactNode, + channel: Channel, + selected: boolean; +} + + +const headerClasses = findByPropsLazy("privateChannelsHeaderContainer"); + +const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; }; + +export let instance: any; +export const forceUpdate = () => instance?.props?._forceUpdate?.(); + +export const settings = definePluginSettings({ + sortDmsByNewestMessage: { + type: OptionType.BOOLEAN, + description: "Sort DMs by newest message", + default: false, + onChange: () => forceUpdate() + }, + + dmSectioncollapsed: { + type: OptionType.BOOLEAN, + description: "Collapse DM sections", + default: false, + onChange: () => forceUpdate() + } +}); export default definePlugin({ name: "PinDMs", description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs", - authors: [Devs.Ven], - + authors: [Devs.Ven, Devs.Aria], settings, contextMenus, - usePinCount(channelIds: string[]) { - const pinnedDms = usePinnedDms(); - // See comment on 2nd patch for reasoning - return channelIds.length ? [pinnedDms.size] : []; - }, - - getChannel(channels: Record, idx: number) { - return channels[getPinAt(idx)]; - }, - - isPinned, - getSnapshot: sortedSnapshot, - - getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) { - if (!isPinned(channelId)) - return ( - (rowHeight + padding) * 2 // header - + rowHeight * snapshotArray.length // pins - + originalOffset // original pin offset minus pins - ); - - return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding; - }, - patches: [ - // Patch DM list { find: ".privateChannelsHeaderContainer,", replacement: [ + // Init { - // filter Discord's privateChannelIds list to remove pins, and pass - // pinCount as prop. This needs to be here so that the entire DM list receives - // updates on pin/unpin - match: /(?<=\i,{channels:\i,)privateChannelIds:(\i),/, - replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1)," + match: /(?<=componentDidMount\(\){).{1,100}scrollToChannel/, + replace: "$self._instance = this;$&" }, { - // sections is an array of numbers, where each element is a section and - // the number is the amount of rows. Add our pinCount in second place - // - Section 1: buttons for pages like Friends & Library - // - Section 2: our pinned dms - // - Section 3: the normal dm list - match: /(?<=renderRow:this\.renderRow,)sections:\[\i,/, - // For some reason, adding our sections when no private channels are ready yet - // makes DMs infinitely load. Thus usePinCount returns either a single element - // array with the count, or an empty array. Due to spreading, only in the former - // case will an element be added to the outer array - // Thanks for the fix, Strencher! - replace: "$&...this.props.pinCount??[]," + // Filter out pinned channels from the private channel list + match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/, + replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))" }, { - // Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages" - // lookbehind is used to lookup parameter name. We could use arguments[0], but - // if children ever is wrapped in an iife, it will break - match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=(\i)=>{.+?)/, - replace: "children:$2.section===1?'Pinned DMs':$1" + // Insert the pinned channels to sections + match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/, + replace: "...$self.makeProps(this,{$&})" + }, + + // Rendering + { + match: /this\.renderDM=\(.+?(\i\.default),{channel.+?this.renderRow=(\i)=>{/, + replace: "$&if($self.isChannelIndex($2.section, $2.row))return $self.renderChannel($2.section,$2.row,$1);" }, { - // Patch channel lookup inside renderDM - // channel=channels[channelIds[row]]; - match: /(?<=renderDM=\((\i),(\i)\)=>{.*?this.state,\i=\i\[\i\],\i=)((\i)\[\i\]);/, - // section 1 is us, manually get our own channel - // section === 1 ? getChannel(channels, row) : channels[channelIds[row]]; - replace: "$1===1?$self.getChannel($4,$2):$3;" + match: /this\.renderSection=(\i)=>{/, + replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);" }, { - // Fix getRowHeight's check for whether this is the DMs section - // DMS (inlined) === section - match: /(?<=getRowHeight=\(.{2,50}?)1===\i/, - // DMS (inlined) === section - 1 - replace: "$&-1" + match: /(?<=span",{)className:\i\.headerText,/, + replace: "...$self.makeSpanProps(),$&" }, + + // Fix Row Height + { + match: /(?<=this\.getRowHeight=.{1,100}return 1===)\i/, + replace: "($&-$self.categoryLen())" + }, + { + match: /this.getRowHeight=\((\i),(\i)\)=>{/, + replace: "$&if($self.isChannelHidden($1,$2))return 0;" + }, + + // Fix ScrollTo { // Override scrollToChannel to properly account for pinned channels match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/, replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)" - } + }, + { + match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/, + replace: "[...$&,...$self.getAllUncollapsedChannels()]" + }, + ] }, + + // forceUpdate moment + // https://regex101.com/r/kDN9fO/1 + { + find: ".FRIENDS},\"friends\"", + replacement: { + match: /(?<=\i=\i=>{).{1,100}premiumTabSelected.{1,800}showDMHeader:.+?,/, + replace: "let forceUpdate = Vencord.Util.useForceUpdater();$&_forceUpdate:forceUpdate," + } + }, + // Fix Alt Up/Down navigation { find: ".Routes.APPLICATION_STORE&&", @@ -118,16 +133,227 @@ export default definePlugin({ // channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)] match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/, // ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c))) - replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))" + replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))" } }, + // fix alt+shift+up/down { find: ".getFlattenedGuildIds()],", replacement: { match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/, - replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))" + replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))" } }, - ] + ], + sections: null as number[] | null, + + set _instance(i: any) { + this.instance = i; + instance = i; + }, + + startAt: StartAt.WebpackReady, + start: init, + flux: { + CONNECTION_OPEN: init, + }, + + isPinned, + categoryLen, + getSections, + getAllUncollapsedChannels, + requireSettingsMenu, + makeProps(instance, { sections }: { sections: number[]; }) { + this.sections = sections; + + this.sections.splice(1, 0, ...this.getPinCount(instance.props.privateChannelIds || [])); + + if (this.instance?.props?.privateChannelIds?.length === 0) { + this.sections[this.sections.length - 1] = 0; + } + + return { + sections: this.sections, + chunkSize: this.getChunkSize(), + }; + }, + + makeSpanProps() { + return { + onClick: () => this.collapseDMList(), + role: "button", + style: { cursor: "pointer" } + }; + }, + + getChunkSize() { + // the chunk size is the amount of rows (measured in pixels) that are rendered at once (probably) + // the higher the chunk size, the more rows are rendered at once + // also if the chunk size is 0 it will render everything at once + + const sections = this.getSections(); + const sectionHeaderSizePx = sections.length * 40; + // (header heights + DM heights + DEFAULT_CHUNK_SIZE) * 1.5 + // we multiply everything by 1.5 so it only gets unmounted after the entire list is off screen + return (sectionHeaderSizePx + sections.reduce((acc, v) => acc += v + 44, 0) + DEFAULT_CHUNK_SIZE) * 1.5; + }, + + getPinCount(channelIds: string[]) { + return channelIds.length ? this.getSections() : []; + }, + + isCategoryIndex(sectionIndex: number) { + return this.sections && sectionIndex > 0 && sectionIndex < this.sections.length - 1; + }, + + isChannelIndex(sectionIndex: number, channelIndex: number) { + if (settings.store.dmSectioncollapsed && sectionIndex !== 0) + return true; + const cat = categories[sectionIndex - 1]; + return this.isCategoryIndex(sectionIndex) && (cat.channels.length === 0 || cat?.channels[channelIndex]); + }, + + isDMSectioncollapsed() { + return settings.store.dmSectioncollapsed; + }, + + collapseDMList() { + // console.log("HI"); + settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed; + forceUpdate(); + }, + + isChannelHidden(categoryIndex: number, channelIndex: number) { + if (categoryIndex === 0) return false; + + if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex) + return true; + + if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false; + + const category = categories[categoryIndex - 1]; + if (!category) return false; + + return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex]; + }, + + getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) { + if (!isPinned(channelId)) + return ( + (rowHeight + padding) * 2 // header + + rowHeight * this.getAllUncollapsedChannels().length // pins + + originalOffset // original pin offset minus pins + ); + + return rowHeight * (this.getAllUncollapsedChannels().indexOf(channelId) + preRenderedChildren) + padding; + }, + + renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => { + const category = categories[section - 1]; + + if (!category) return null; + + return ( +

{ + await collapseCategory(category.id, !category.collapsed); + forceUpdate(); + }} + onContextMenu={e => { + ContextMenuApi.openContextMenu(e, () => ( + FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} + color="danger" + aria-label="Pin DMs Category Menu" + > + openCategoryModal(category.id, null)} + /> + + { + canMoveCategory(category.id) && ( + <> + { + canMoveCategoryInDirection(category.id, -1) && moveCategory(category.id, -1).then(() => forceUpdate())} + /> + } + { + canMoveCategoryInDirection(category.id, 1) && moveCategory(category.id, 1).then(() => forceUpdate())} + /> + } + + + ) + } + + + removeCategory(category.id).then(() => forceUpdate())} + /> + + + + )); + }} + > + + {category?.name ?? "uh oh"} + + +

+ ); + }), + + renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType) { + const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels); + + if (!channel || !category) return null; + if (this.isChannelHidden(sectionIndex, index)) return null; + + return ( + + {channel.id} + + ); + }, + + + getChannel(sectionIndex: number, index: number, channels: Record) { + const category = categories[sectionIndex - 1]; + if (!category) return { channel: null, category: null }; + + const channelId = this.getCategoryChannels(category)[index]; + + return { channel: channels[channelId], category }; + }, + + getCategoryChannels(category: Category) { + if (category.channels.length === 0) return []; + + if (settings.store.sortDmsByNewestMessage) { + return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category.channels.includes(c)); + } + + return category?.channels ?? []; + } }); diff --git a/src/plugins/pinDms/settings.ts b/src/plugins/pinDms/settings.ts deleted file mode 100644 index 1e0244ed7..000000000 --- a/src/plugins/pinDms/settings.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { definePluginSettings, Settings, useSettings } from "@api/Settings"; -import { OptionType } from "@utils/types"; -import { findStoreLazy } from "@webpack"; - -export const enum PinOrder { - LastMessage, - Custom -} - -export const settings = definePluginSettings({ - pinOrder: { - type: OptionType.SELECT, - description: "Which order should pinned DMs be displayed in?", - options: [ - { label: "Most recent message", value: PinOrder.LastMessage, default: true }, - { label: "Custom (right click channels to reorder)", value: PinOrder.Custom } - ] - } -}); - -const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore"); - -export let snapshotArray: string[]; -let snapshot: Set | undefined; - -const getArray = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined; -const save = (pins: string[]) => { - snapshot = void 0; - Settings.plugins.PinDMs.pinnedDMs = pins.join(","); -}; -const takeSnapshot = () => { - snapshotArray = getArray() ?? []; - return snapshot = new Set(snapshotArray); -}; -const requireSnapshot = () => snapshot ?? takeSnapshot(); - -export function usePinnedDms() { - useSettings(["plugins.PinDMs.pinnedDMs"]); - - return requireSnapshot(); -} - -export function isPinned(id: string) { - return requireSnapshot().has(id); -} - -export function togglePin(id: string) { - const snapshot = requireSnapshot(); - if (!snapshot.delete(id)) { - snapshot.add(id); - } - - save([...snapshot]); -} - -export function sortedSnapshot() { - requireSnapshot(); - if (settings.store.pinOrder === PinOrder.LastMessage) - return PrivateChannelSortStore.getPrivateChannelIds().filter(isPinned); - - return snapshotArray; -} - -export function getPinAt(idx: number) { - return sortedSnapshot()[idx]; -} - -export function movePin(id: string, direction: -1 | 1) { - const pins = getArray()!; - const a = pins.indexOf(id); - const b = a + direction; - - [pins[a], pins[b]] = [pins[b], pins[a]]; - - save(pins); -} diff --git a/src/plugins/pinDms/styles.css b/src/plugins/pinDms/styles.css new file mode 100644 index 000000000..173f0f76b --- /dev/null +++ b/src/plugins/pinDms/styles.css @@ -0,0 +1,37 @@ +.vc-pindms-section-container { + box-sizing: border-box; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-transform: uppercase; + font-size: 12px; + line-height: 16px; + letter-spacing: .02em; + font-family: var(--font-display); + font-weight: 600; + flex: 1 1 auto; + color: var(--channels-default); + cursor: pointer; +} + +.vc-pindms-modal-content { + display: grid; + justify-content: center; + padding: 1rem; + gap: 1.5rem; +} + +.vc-pindms-modal-content [class^="defaultContainer"] { + display: none; +} + +.vc-pindms-collapse-icon { + width: 16px; + height: 16px; + color: var(--interactive-normal); + transform: rotate(90deg) +} + +.vc-pindms-collapsed .vc-pindms-collapse-icon { + transform: rotate(0deg); +} From 5646fe402ac0da45e9a6a9f25d2594cb721527cf Mon Sep 17 00:00:00 2001 From: MrDiamondDog <84212701+MrDiamondDog@users.noreply.github.com> Date: Thu, 21 Mar 2024 17:59:25 -0600 Subject: [PATCH 132/356] feat(PatchHelper): Paste Full Patch (#1982) --- src/components/CheckedTextInput.tsx | 6 +- .../VencordSettings/PatchHelperTab.tsx | 65 ++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/components/CheckedTextInput.tsx b/src/components/CheckedTextInput.tsx index cf4aa119b..de2d7ae04 100644 --- a/src/components/CheckedTextInput.tsx +++ b/src/components/CheckedTextInput.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { React, TextInput } from "@webpack/common"; +import { React, TextInput, useEffect } from "@webpack/common"; // TODO: Refactor settings to use this as well interface TextInputProps { @@ -55,6 +55,10 @@ export function CheckedTextInput({ value: initialValue, onChange, validate }: Te } } + useEffect(() => { + handleChange(initialValue); + }, [initialValue]); + return ( <> (""); + const [fullPatchError, setFullPatchError] = React.useState(""); + + function update() { + if (fullPatch === "") { + setFullPatchError(""); + + setFind(""); + setMatch(""); + setReplacement(""); + return; + } + + try { + const parsed = (0, eval)(`(${fullPatch})`) as Patch; + + if (!parsed.find) throw new Error("No 'find' field"); + if (!parsed.replacement) throw new Error("No 'replacement' field"); + + if (parsed.replacement instanceof Array) { + if (parsed.replacement.length === 0) throw new Error("Invalid replacement"); + + parsed.replacement = { + match: parsed.replacement[0].match, + replace: parsed.replacement[0].replace + }; + } + + if (!parsed.replacement.match) throw new Error("No 'replacement.match' field"); + if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field"); + + setFind(parsed.find); + setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match); + setReplacement(parsed.replacement.replace); + setFullPatchError(""); + } catch (e) { + setFullPatchError((e as Error).message); + } + } + + return <> + Paste your full JSON patch here to fill out the fields +