From 30ac25607023752031aa98060cbf8a736109992d Mon Sep 17 00:00:00 2001 From: V Date: Sun, 24 Sep 2023 16:02:18 +0200 Subject: migrate all plugins to folders --- src/plugins/alwaysAnimate.ts | 37 - src/plugins/alwaysAnimate/index.ts | 37 + src/plugins/alwaysTrust.ts | 42 -- src/plugins/alwaysTrust/index.ts | 42 ++ src/plugins/anonymiseFileNames.ts | 94 --- src/plugins/anonymiseFileNames/index.ts | 94 +++ src/plugins/arRPC.web.tsx | 109 --- src/plugins/arRPC.web/index.tsx | 109 +++ src/plugins/banger.ts | 43 -- src/plugins/banger/index.ts | 43 ++ src/plugins/betterGifAltText.ts | 69 -- src/plugins/betterGifAltText/index.ts | 69 ++ src/plugins/betterNotes.ts | 60 -- src/plugins/betterNotes/index.ts | 60 ++ src/plugins/betterRoleDot.ts | 93 --- src/plugins/betterRoleDot/index.ts | 93 +++ src/plugins/betterUploadButton.ts | 38 - src/plugins/betterUploadButton/index.ts | 38 + src/plugins/blurNsfw.ts | 77 -- src/plugins/blurNsfw/index.ts | 77 ++ src/plugins/callTimer.tsx | 95 --- src/plugins/callTimer/index.tsx | 95 +++ src/plugins/colorSighted.ts | 42 -- src/plugins/colorSighted/index.ts | 42 ++ src/plugins/consoleShortcuts.ts | 112 --- src/plugins/consoleShortcuts/index.ts | 112 +++ src/plugins/copyUserURLs.tsx | 55 -- src/plugins/copyUserURLs/index.tsx | 55 ++ src/plugins/crashHandler.ts | 158 ----- src/plugins/crashHandler/index.ts | 158 +++++ src/plugins/customRPC.tsx | 425 ------------ src/plugins/customRPC/index.tsx | 425 ++++++++++++ src/plugins/devCompanion.dev.tsx | 260 ------- src/plugins/devCompanion.dev/index.tsx | 260 +++++++ src/plugins/disableDMCallIdle.ts | 35 - src/plugins/disableDMCallIdle/index.ts | 35 + src/plugins/emoteCloner.tsx | 373 ---------- src/plugins/emoteCloner/index.tsx | 373 ++++++++++ src/plugins/experiments.tsx | 137 ---- src/plugins/experiments/index.tsx | 137 ++++ src/plugins/f8break.ts | 42 -- src/plugins/f8break/index.ts | 42 ++ src/plugins/fakeNitro.ts | 811 ---------------------- src/plugins/fakeNitro/index.ts | 811 ++++++++++++++++++++++ src/plugins/fakeProfileThemes.tsx | 145 ---- src/plugins/fakeProfileThemes/index.tsx | 145 ++++ src/plugins/fixSpotifyEmbeds.desktop.ts | 26 - src/plugins/fixSpotifyEmbeds.desktop/index.ts | 26 + src/plugins/forceOwnerCrown.ts | 58 -- src/plugins/forceOwnerCrown/index.ts | 58 ++ src/plugins/friendInvites.ts | 121 ---- src/plugins/friendInvites/index.ts | 121 ++++ src/plugins/gifPaste.ts | 47 -- src/plugins/gifPaste/index.ts | 47 ++ src/plugins/greetStickerPicker.tsx | 188 ----- src/plugins/greetStickerPicker/index.tsx | 188 +++++ src/plugins/hideAttachments.tsx | 95 --- src/plugins/hideAttachments/index.tsx | 95 +++ src/plugins/iLoveSpam.ts | 35 - src/plugins/iLoveSpam/index.ts | 35 + src/plugins/ignoreActivities.tsx | 233 ------- src/plugins/ignoreActivities/index.tsx | 233 +++++++ src/plugins/keepCurrentChannel.ts | 90 --- src/plugins/keepCurrentChannel/index.ts | 90 +++ src/plugins/lastfm.tsx | 337 --------- src/plugins/lastfm/index.tsx | 337 +++++++++ src/plugins/loadingQuotes.ts | 86 --- src/plugins/loadingQuotes/index.ts | 86 +++ src/plugins/memberCount.tsx | 116 ---- src/plugins/memberCount/index.tsx | 116 ++++ src/plugins/messageClickActions.ts | 113 --- src/plugins/messageClickActions/index.ts | 113 +++ src/plugins/messageLinkEmbeds.tsx | 402 ----------- src/plugins/messageLinkEmbeds/index.tsx | 402 +++++++++++ src/plugins/messageTags.ts | 249 ------- src/plugins/messageTags/index.ts | 249 +++++++ src/plugins/moreCommands.ts | 66 -- src/plugins/moreCommands/index.ts | 66 ++ src/plugins/moreKaomoji.ts | 48 -- src/plugins/moreKaomoji/index.ts | 48 ++ src/plugins/moreUserTags.tsx | 383 ---------- src/plugins/moreUserTags/index.tsx | 383 ++++++++++ src/plugins/moyai.ts | 177 ----- src/plugins/moyai/index.ts | 177 +++++ src/plugins/muteNewGuild.tsx | 74 -- src/plugins/muteNewGuild/index.tsx | 74 ++ src/plugins/mutualGroupDMs.tsx | 103 --- src/plugins/mutualGroupDMs/index.tsx | 103 +++ src/plugins/noBlockedMessages.ts | 64 -- src/plugins/noBlockedMessages/index.ts | 64 ++ src/plugins/noDevtoolsWarning.ts | 33 - src/plugins/noDevtoolsWarning/index.ts | 33 + src/plugins/noF1.ts | 35 - src/plugins/noF1/index.ts | 35 + src/plugins/noPendingCount.ts | 96 --- src/plugins/noPendingCount/index.ts | 96 +++ src/plugins/noProfileThemes.ts | 53 -- src/plugins/noProfileThemes/index.ts | 53 ++ src/plugins/noRPC.discordDesktop.ts | 35 - src/plugins/noRPC.discordDesktop/index.ts | 35 + src/plugins/noReplyMention.tsx | 74 -- src/plugins/noReplyMention/index.tsx | 74 ++ src/plugins/noScreensharePreview.ts | 38 - src/plugins/noScreensharePreview/index.ts | 38 + src/plugins/noSystemBadge.discordDesktop.ts | 41 -- src/plugins/noSystemBadge.discordDesktop/index.ts | 41 ++ src/plugins/noUnblockToJump.ts | 50 -- src/plugins/noUnblockToJump/index.ts | 50 ++ src/plugins/nsfwGateBypass.ts | 35 - src/plugins/nsfwGateBypass/index.ts | 35 + src/plugins/oneko.ts | 40 -- src/plugins/oneko/index.ts | 40 ++ src/plugins/openInApp.ts | 147 ---- src/plugins/openInApp/index.ts | 147 ++++ src/plugins/partyMode.ts | 105 --- src/plugins/partyMode/index.ts | 105 +++ src/plugins/petpet.ts | 182 ----- src/plugins/petpet/index.ts | 182 +++++ src/plugins/pictureInPicture.tsx | 83 --- src/plugins/pictureInPicture/index.tsx | 83 +++ src/plugins/plainFolderIcon.ts | 33 - src/plugins/plainFolderIcon/index.ts | 33 + src/plugins/platformIndicators.tsx | 261 ------- src/plugins/platformIndicators/index.tsx | 261 +++++++ src/plugins/quickMention.tsx | 59 -- src/plugins/quickMention/index.tsx | 59 ++ src/plugins/quickReply.ts | 216 ------ src/plugins/quickReply/index.ts | 216 ++++++ src/plugins/reactErrorDecoder.ts | 59 -- src/plugins/reactErrorDecoder/index.ts | 59 ++ src/plugins/readAllNotificationsButton.tsx | 71 -- src/plugins/readAllNotificationsButton/index.tsx | 71 ++ src/plugins/revealAllSpoilers.ts | 58 -- src/plugins/revealAllSpoilers/index.ts | 58 ++ src/plugins/reverseImageSearch.tsx | 120 ---- src/plugins/reverseImageSearch/index.tsx | 120 ++++ src/plugins/roleColorEverywhere.tsx | 126 ---- src/plugins/roleColorEverywhere/index.tsx | 126 ++++ src/plugins/secretRingTone.ts | 35 - src/plugins/secretRingTone/index.ts | 35 + src/plugins/serverListIndicators.tsx | 137 ---- src/plugins/serverListIndicators/index.tsx | 137 ++++ src/plugins/showAllMessageButtons.ts | 37 - src/plugins/showAllMessageButtons/index.ts | 37 + src/plugins/showTimeouts.ts | 35 - src/plugins/showTimeouts/index.ts | 35 + src/plugins/silentMessageToggle.tsx | 120 ---- src/plugins/silentMessageToggle/index.tsx | 120 ++++ src/plugins/silentTyping.tsx | 124 ---- src/plugins/silentTyping/index.tsx | 124 ++++ src/plugins/sortFriendRequests.tsx | 74 -- src/plugins/sortFriendRequests/index.tsx | 74 ++ src/plugins/spotifyCrack.ts | 69 -- src/plugins/spotifyCrack/index.ts | 69 ++ src/plugins/spotifyShareCommands.ts | 138 ---- src/plugins/spotifyShareCommands/index.ts | 138 ++++ src/plugins/textReplace.tsx | 267 ------- src/plugins/textReplace/index.tsx | 267 +++++++ src/plugins/timeBarAllActivities.ts | 35 - src/plugins/timeBarAllActivities/index.ts | 35 + src/plugins/typingIndicator.tsx | 140 ---- src/plugins/typingIndicator/index.tsx | 140 ++++ src/plugins/typingTweaks.tsx | 139 ---- src/plugins/typingTweaks/index.tsx | 139 ++++ src/plugins/unindent.ts | 67 -- src/plugins/unindent/index.ts | 67 ++ src/plugins/unsuppressEmbeds.tsx | 67 -- src/plugins/unsuppressEmbeds/index.tsx | 67 ++ src/plugins/urbanDictionary.ts | 91 --- src/plugins/urbanDictionary/index.ts | 91 +++ src/plugins/validUser.tsx | 144 ---- src/plugins/validUser/index.tsx | 144 ++++ src/plugins/vcDoubleClick.ts | 86 --- src/plugins/vcDoubleClick/index.ts | 86 +++ src/plugins/vcNarrator.tsx | 341 --------- src/plugins/vcNarrator/index.tsx | 341 +++++++++ src/plugins/viewIcons.tsx | 200 ------ src/plugins/viewIcons/index.tsx | 200 ++++++ src/plugins/viewRaw.tsx | 198 ------ src/plugins/viewRaw/index.tsx | 198 ++++++ src/plugins/volumeBooster.discordDesktop.ts | 86 --- src/plugins/volumeBooster.discordDesktop/index.ts | 86 +++ src/plugins/webContextMenus.web.ts | 232 ------- src/plugins/webContextMenus.web/index.ts | 232 +++++++ src/plugins/webKeybinds.vencordDesktop.ts | 79 --- src/plugins/webKeybinds.vencordDesktop/index.ts | 79 +++ src/plugins/whoReacted.tsx | 262 ------- src/plugins/whoReacted/index.tsx | 262 +++++++ src/plugins/wikisearch.ts | 110 --- src/plugins/wikisearch/index.ts | 110 +++ 190 files changed, 11826 insertions(+), 11826 deletions(-) delete mode 100644 src/plugins/alwaysAnimate.ts create mode 100644 src/plugins/alwaysAnimate/index.ts delete mode 100644 src/plugins/alwaysTrust.ts create mode 100644 src/plugins/alwaysTrust/index.ts delete mode 100644 src/plugins/anonymiseFileNames.ts create mode 100644 src/plugins/anonymiseFileNames/index.ts delete mode 100644 src/plugins/arRPC.web.tsx create mode 100644 src/plugins/arRPC.web/index.tsx delete mode 100644 src/plugins/banger.ts create mode 100644 src/plugins/banger/index.ts delete mode 100644 src/plugins/betterGifAltText.ts create mode 100644 src/plugins/betterGifAltText/index.ts delete mode 100644 src/plugins/betterNotes.ts create mode 100644 src/plugins/betterNotes/index.ts delete mode 100644 src/plugins/betterRoleDot.ts create mode 100644 src/plugins/betterRoleDot/index.ts delete mode 100644 src/plugins/betterUploadButton.ts create mode 100644 src/plugins/betterUploadButton/index.ts delete mode 100644 src/plugins/blurNsfw.ts create mode 100644 src/plugins/blurNsfw/index.ts delete mode 100644 src/plugins/callTimer.tsx create mode 100644 src/plugins/callTimer/index.tsx delete mode 100644 src/plugins/colorSighted.ts create mode 100644 src/plugins/colorSighted/index.ts delete mode 100644 src/plugins/consoleShortcuts.ts create mode 100644 src/plugins/consoleShortcuts/index.ts delete mode 100644 src/plugins/copyUserURLs.tsx create mode 100644 src/plugins/copyUserURLs/index.tsx delete mode 100644 src/plugins/crashHandler.ts create mode 100644 src/plugins/crashHandler/index.ts delete mode 100644 src/plugins/customRPC.tsx create mode 100644 src/plugins/customRPC/index.tsx delete mode 100644 src/plugins/devCompanion.dev.tsx create mode 100644 src/plugins/devCompanion.dev/index.tsx delete mode 100644 src/plugins/disableDMCallIdle.ts create mode 100644 src/plugins/disableDMCallIdle/index.ts delete mode 100644 src/plugins/emoteCloner.tsx create mode 100644 src/plugins/emoteCloner/index.tsx delete mode 100644 src/plugins/experiments.tsx create mode 100644 src/plugins/experiments/index.tsx delete mode 100644 src/plugins/f8break.ts create mode 100644 src/plugins/f8break/index.ts delete mode 100644 src/plugins/fakeNitro.ts create mode 100644 src/plugins/fakeNitro/index.ts delete mode 100644 src/plugins/fakeProfileThemes.tsx create mode 100644 src/plugins/fakeProfileThemes/index.tsx delete mode 100644 src/plugins/fixSpotifyEmbeds.desktop.ts create mode 100644 src/plugins/fixSpotifyEmbeds.desktop/index.ts delete mode 100644 src/plugins/forceOwnerCrown.ts create mode 100644 src/plugins/forceOwnerCrown/index.ts delete mode 100644 src/plugins/friendInvites.ts create mode 100644 src/plugins/friendInvites/index.ts delete mode 100644 src/plugins/gifPaste.ts create mode 100644 src/plugins/gifPaste/index.ts delete mode 100644 src/plugins/greetStickerPicker.tsx create mode 100644 src/plugins/greetStickerPicker/index.tsx delete mode 100644 src/plugins/hideAttachments.tsx create mode 100644 src/plugins/hideAttachments/index.tsx delete mode 100644 src/plugins/iLoveSpam.ts create mode 100644 src/plugins/iLoveSpam/index.ts delete mode 100644 src/plugins/ignoreActivities.tsx create mode 100644 src/plugins/ignoreActivities/index.tsx delete mode 100644 src/plugins/keepCurrentChannel.ts create mode 100644 src/plugins/keepCurrentChannel/index.ts delete mode 100644 src/plugins/lastfm.tsx create mode 100644 src/plugins/lastfm/index.tsx delete mode 100644 src/plugins/loadingQuotes.ts create mode 100644 src/plugins/loadingQuotes/index.ts delete mode 100644 src/plugins/memberCount.tsx create mode 100644 src/plugins/memberCount/index.tsx delete mode 100644 src/plugins/messageClickActions.ts create mode 100644 src/plugins/messageClickActions/index.ts delete mode 100644 src/plugins/messageLinkEmbeds.tsx create mode 100644 src/plugins/messageLinkEmbeds/index.tsx delete mode 100644 src/plugins/messageTags.ts create mode 100644 src/plugins/messageTags/index.ts delete mode 100644 src/plugins/moreCommands.ts create mode 100644 src/plugins/moreCommands/index.ts delete mode 100644 src/plugins/moreKaomoji.ts create mode 100644 src/plugins/moreKaomoji/index.ts delete mode 100644 src/plugins/moreUserTags.tsx create mode 100644 src/plugins/moreUserTags/index.tsx delete mode 100644 src/plugins/moyai.ts create mode 100644 src/plugins/moyai/index.ts delete mode 100644 src/plugins/muteNewGuild.tsx create mode 100644 src/plugins/muteNewGuild/index.tsx delete mode 100644 src/plugins/mutualGroupDMs.tsx create mode 100644 src/plugins/mutualGroupDMs/index.tsx delete mode 100644 src/plugins/noBlockedMessages.ts create mode 100644 src/plugins/noBlockedMessages/index.ts delete mode 100644 src/plugins/noDevtoolsWarning.ts create mode 100644 src/plugins/noDevtoolsWarning/index.ts delete mode 100644 src/plugins/noF1.ts create mode 100644 src/plugins/noF1/index.ts delete mode 100644 src/plugins/noPendingCount.ts create mode 100644 src/plugins/noPendingCount/index.ts delete mode 100644 src/plugins/noProfileThemes.ts create mode 100644 src/plugins/noProfileThemes/index.ts delete mode 100644 src/plugins/noRPC.discordDesktop.ts create mode 100644 src/plugins/noRPC.discordDesktop/index.ts delete mode 100644 src/plugins/noReplyMention.tsx create mode 100644 src/plugins/noReplyMention/index.tsx delete mode 100644 src/plugins/noScreensharePreview.ts create mode 100644 src/plugins/noScreensharePreview/index.ts delete mode 100644 src/plugins/noSystemBadge.discordDesktop.ts create mode 100644 src/plugins/noSystemBadge.discordDesktop/index.ts delete mode 100644 src/plugins/noUnblockToJump.ts create mode 100644 src/plugins/noUnblockToJump/index.ts delete mode 100644 src/plugins/nsfwGateBypass.ts create mode 100644 src/plugins/nsfwGateBypass/index.ts delete mode 100644 src/plugins/oneko.ts create mode 100644 src/plugins/oneko/index.ts delete mode 100644 src/plugins/openInApp.ts create mode 100644 src/plugins/openInApp/index.ts delete mode 100644 src/plugins/partyMode.ts create mode 100644 src/plugins/partyMode/index.ts delete mode 100644 src/plugins/petpet.ts create mode 100644 src/plugins/petpet/index.ts delete mode 100644 src/plugins/pictureInPicture.tsx create mode 100644 src/plugins/pictureInPicture/index.tsx delete mode 100644 src/plugins/plainFolderIcon.ts create mode 100644 src/plugins/plainFolderIcon/index.ts delete mode 100644 src/plugins/platformIndicators.tsx create mode 100644 src/plugins/platformIndicators/index.tsx delete mode 100644 src/plugins/quickMention.tsx create mode 100644 src/plugins/quickMention/index.tsx delete mode 100644 src/plugins/quickReply.ts create mode 100644 src/plugins/quickReply/index.ts delete mode 100644 src/plugins/reactErrorDecoder.ts create mode 100644 src/plugins/reactErrorDecoder/index.ts delete mode 100644 src/plugins/readAllNotificationsButton.tsx create mode 100644 src/plugins/readAllNotificationsButton/index.tsx delete mode 100644 src/plugins/revealAllSpoilers.ts create mode 100644 src/plugins/revealAllSpoilers/index.ts delete mode 100644 src/plugins/reverseImageSearch.tsx create mode 100644 src/plugins/reverseImageSearch/index.tsx delete mode 100644 src/plugins/roleColorEverywhere.tsx create mode 100644 src/plugins/roleColorEverywhere/index.tsx delete mode 100644 src/plugins/secretRingTone.ts create mode 100644 src/plugins/secretRingTone/index.ts delete mode 100644 src/plugins/serverListIndicators.tsx create mode 100644 src/plugins/serverListIndicators/index.tsx delete mode 100644 src/plugins/showAllMessageButtons.ts create mode 100644 src/plugins/showAllMessageButtons/index.ts delete mode 100644 src/plugins/showTimeouts.ts create mode 100644 src/plugins/showTimeouts/index.ts delete mode 100644 src/plugins/silentMessageToggle.tsx create mode 100644 src/plugins/silentMessageToggle/index.tsx delete mode 100644 src/plugins/silentTyping.tsx create mode 100644 src/plugins/silentTyping/index.tsx delete mode 100644 src/plugins/sortFriendRequests.tsx create mode 100644 src/plugins/sortFriendRequests/index.tsx delete mode 100644 src/plugins/spotifyCrack.ts create mode 100644 src/plugins/spotifyCrack/index.ts delete mode 100644 src/plugins/spotifyShareCommands.ts create mode 100644 src/plugins/spotifyShareCommands/index.ts delete mode 100644 src/plugins/textReplace.tsx create mode 100644 src/plugins/textReplace/index.tsx delete mode 100644 src/plugins/timeBarAllActivities.ts create mode 100644 src/plugins/timeBarAllActivities/index.ts delete mode 100644 src/plugins/typingIndicator.tsx create mode 100644 src/plugins/typingIndicator/index.tsx delete mode 100644 src/plugins/typingTweaks.tsx create mode 100644 src/plugins/typingTweaks/index.tsx delete mode 100644 src/plugins/unindent.ts create mode 100644 src/plugins/unindent/index.ts delete mode 100644 src/plugins/unsuppressEmbeds.tsx create mode 100644 src/plugins/unsuppressEmbeds/index.tsx delete mode 100644 src/plugins/urbanDictionary.ts create mode 100644 src/plugins/urbanDictionary/index.ts delete mode 100644 src/plugins/validUser.tsx create mode 100644 src/plugins/validUser/index.tsx delete mode 100644 src/plugins/vcDoubleClick.ts create mode 100644 src/plugins/vcDoubleClick/index.ts delete mode 100644 src/plugins/vcNarrator.tsx create mode 100644 src/plugins/vcNarrator/index.tsx delete mode 100644 src/plugins/viewIcons.tsx create mode 100644 src/plugins/viewIcons/index.tsx delete mode 100644 src/plugins/viewRaw.tsx create mode 100644 src/plugins/viewRaw/index.tsx delete mode 100644 src/plugins/volumeBooster.discordDesktop.ts create mode 100644 src/plugins/volumeBooster.discordDesktop/index.ts delete mode 100644 src/plugins/webContextMenus.web.ts create mode 100644 src/plugins/webContextMenus.web/index.ts delete mode 100644 src/plugins/webKeybinds.vencordDesktop.ts create mode 100644 src/plugins/webKeybinds.vencordDesktop/index.ts delete mode 100644 src/plugins/whoReacted.tsx create mode 100644 src/plugins/whoReacted/index.tsx delete mode 100644 src/plugins/wikisearch.ts create mode 100644 src/plugins/wikisearch/index.ts (limited to 'src/plugins') diff --git a/src/plugins/alwaysAnimate.ts b/src/plugins/alwaysAnimate.ts deleted file mode 100644 index f3ae27a..0000000 --- a/src/plugins/alwaysAnimate.ts +++ /dev/null @@ -1,37 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "AlwaysAnimate", - description: "Animates anything that can be animated, besides status emojis.", - authors: [Devs.FieryFlames], - - patches: [ - { - find: ".canAnimate", - all: true, - replacement: { - match: /\.canAnimate\b/g, - replace: ".canAnimate || true" - } - } - ] -}); diff --git a/src/plugins/alwaysAnimate/index.ts b/src/plugins/alwaysAnimate/index.ts new file mode 100644 index 0000000..f3ae27a --- /dev/null +++ b/src/plugins/alwaysAnimate/index.ts @@ -0,0 +1,37 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "AlwaysAnimate", + description: "Animates anything that can be animated, besides status emojis.", + authors: [Devs.FieryFlames], + + patches: [ + { + find: ".canAnimate", + all: true, + replacement: { + match: /\.canAnimate\b/g, + replace: ".canAnimate || true" + } + } + ] +}); diff --git a/src/plugins/alwaysTrust.ts b/src/plugins/alwaysTrust.ts deleted file mode 100644 index 79193b0..0000000 --- a/src/plugins/alwaysTrust.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "AlwaysTrust", - description: "Removes the annoying untrusted domain and suspicious file popup", - authors: [Devs.zt], - patches: [ - { - find: ".displayName=\"MaskedLinkStore\"", - replacement: { - match: /\.isTrustedDomain=function\(.\){return.+?};/, - replace: ".isTrustedDomain=function(){return true};" - } - }, - { - find: '"7z","ade","adp"', - replacement: { - match: /JSON\.parse\('\[.+?'\)/, - replace: "[]" - } - } - ] -}); diff --git a/src/plugins/alwaysTrust/index.ts b/src/plugins/alwaysTrust/index.ts new file mode 100644 index 0000000..79193b0 --- /dev/null +++ b/src/plugins/alwaysTrust/index.ts @@ -0,0 +1,42 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "AlwaysTrust", + description: "Removes the annoying untrusted domain and suspicious file popup", + authors: [Devs.zt], + patches: [ + { + find: ".displayName=\"MaskedLinkStore\"", + replacement: { + match: /\.isTrustedDomain=function\(.\){return.+?};/, + replace: ".isTrustedDomain=function(){return true};" + } + }, + { + find: '"7z","ade","adp"', + replacement: { + match: /JSON\.parse\('\[.+?'\)/, + replace: "[]" + } + } + ] +}); diff --git a/src/plugins/anonymiseFileNames.ts b/src/plugins/anonymiseFileNames.ts deleted file mode 100644 index 9e69d7a..0000000 --- a/src/plugins/anonymiseFileNames.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.ts b/src/plugins/anonymiseFileNames/index.ts new file mode 100644 index 0000000..9e69d7a --- /dev/null +++ b/src/plugins/anonymiseFileNames/index.ts @@ -0,0 +1,94 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +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/arRPC.web.tsx b/src/plugins/arRPC.web.tsx deleted file mode 100644 index f0d4841..0000000 --- a/src/plugins/arRPC.web.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 OpenAsar - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { popNotice, showNotice } from "@api/Notices"; -import { Link } from "@components/Link"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { filters, findByCodeLazy, mapMangledModuleLazy } from "@webpack"; -import { FluxDispatcher, Forms, Toasts } from "@webpack/common"; - -const assetManager = mapMangledModuleLazy( - "getAssetImage: size must === [number, number] for Twitch", - { - getAsset: filters.byCode("apply("), - } -); - -const lookupRpcApp = findByCodeLazy(".APPLICATION_RPC("); - -async function lookupAsset(applicationId: string, key: string): Promise { - return (await assetManager.getAsset(applicationId, [key, undefined]))[0]; -} - -const apps: any = {}; -async function lookupApp(applicationId: string): Promise { - const socket: any = {}; - await lookupRpcApp(socket, applicationId); - return socket.application; -} - -let ws: WebSocket; -export default definePlugin({ - name: "WebRichPresence (arRPC)", - description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)", - authors: [Devs.Ducko], - - settingsAboutComponent: () => ( - <> - How to use arRPC - - Follow the instructions in the GitHub repo to get the server running, and then enable the plugin. - - - ), - - async start() { - // ArmCord comes with its own arRPC implementation, so this plugin just confuses users - if ("armcord" in window) return; - - if (ws) ws.close(); - ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket - - ws.onmessage = async e => { // on message, set status to data - const data = JSON.parse(e.data); - - if (data.activity?.assets?.large_image) data.activity.assets.large_image = await lookupAsset(data.activity.application_id, data.activity.assets.large_image); - if (data.activity?.assets?.small_image) data.activity.assets.small_image = await lookupAsset(data.activity.application_id, data.activity.assets.small_image); - - if (data.activity) { - const appId = data.activity.application_id; - apps[appId] ||= await lookupApp(appId); - - const app = apps[appId]; - data.activity.name ||= app.name; - } - - FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data }); - }; - - const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 1000)); // check if open after 1s - if (!connectionSuccessful) { - showNotice("Failed to connect to arRPC, is it running?", "Retry", () => { // show notice about failure to connect, with retry/ignore - popNotice(); - this.start(); - }); - return; - } - - Toasts.show({ // show toast on success - message: "Connected to arRPC", - type: Toasts.Type.SUCCESS, - id: Toasts.genId(), - options: { - duration: 1000, - position: Toasts.Position.BOTTOM - } - }); - }, - - stop() { - FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); // clear status - ws?.close(); // close WebSocket - } -}); diff --git a/src/plugins/arRPC.web/index.tsx b/src/plugins/arRPC.web/index.tsx new file mode 100644 index 0000000..f0d4841 --- /dev/null +++ b/src/plugins/arRPC.web/index.tsx @@ -0,0 +1,109 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 OpenAsar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { popNotice, showNotice } from "@api/Notices"; +import { Link } from "@components/Link"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { filters, findByCodeLazy, mapMangledModuleLazy } from "@webpack"; +import { FluxDispatcher, Forms, Toasts } from "@webpack/common"; + +const assetManager = mapMangledModuleLazy( + "getAssetImage: size must === [number, number] for Twitch", + { + getAsset: filters.byCode("apply("), + } +); + +const lookupRpcApp = findByCodeLazy(".APPLICATION_RPC("); + +async function lookupAsset(applicationId: string, key: string): Promise { + return (await assetManager.getAsset(applicationId, [key, undefined]))[0]; +} + +const apps: any = {}; +async function lookupApp(applicationId: string): Promise { + const socket: any = {}; + await lookupRpcApp(socket, applicationId); + return socket.application; +} + +let ws: WebSocket; +export default definePlugin({ + name: "WebRichPresence (arRPC)", + description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)", + authors: [Devs.Ducko], + + settingsAboutComponent: () => ( + <> + How to use arRPC + + Follow the instructions in the GitHub repo to get the server running, and then enable the plugin. + + + ), + + async start() { + // ArmCord comes with its own arRPC implementation, so this plugin just confuses users + if ("armcord" in window) return; + + if (ws) ws.close(); + ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket + + ws.onmessage = async e => { // on message, set status to data + const data = JSON.parse(e.data); + + if (data.activity?.assets?.large_image) data.activity.assets.large_image = await lookupAsset(data.activity.application_id, data.activity.assets.large_image); + if (data.activity?.assets?.small_image) data.activity.assets.small_image = await lookupAsset(data.activity.application_id, data.activity.assets.small_image); + + if (data.activity) { + const appId = data.activity.application_id; + apps[appId] ||= await lookupApp(appId); + + const app = apps[appId]; + data.activity.name ||= app.name; + } + + FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data }); + }; + + const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 1000)); // check if open after 1s + if (!connectionSuccessful) { + showNotice("Failed to connect to arRPC, is it running?", "Retry", () => { // show notice about failure to connect, with retry/ignore + popNotice(); + this.start(); + }); + return; + } + + Toasts.show({ // show toast on success + message: "Connected to arRPC", + type: Toasts.Type.SUCCESS, + id: Toasts.genId(), + options: { + duration: 1000, + position: Toasts.Position.BOTTOM + } + }); + }, + + stop() { + FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); // clear status + ws?.close(); // close WebSocket + } +}); diff --git a/src/plugins/banger.ts b/src/plugins/banger.ts deleted file mode 100644 index 68163cb..0000000 --- a/src/plugins/banger.ts +++ /dev/null @@ -1,43 +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 { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -export default definePlugin({ - name: "BANger", - description: "Replaces the GIF in the ban dialogue with a custom one.", - authors: [Devs.Xinto, Devs.Glitch], - patches: [ - { - find: "BAN_CONFIRM_TITLE.", - replacement: { - match: /src:\w\(\d+\)/g, - replace: "src: Vencord.Settings.plugins.BANger.source" - } - } - ], - options: { - source: { - description: "Source to replace ban GIF with (Video or Gif)", - type: OptionType.STRING, - default: "https://i.imgur.com/wp5q52C.mp4", - restartNeeded: true, - } - } -}); diff --git a/src/plugins/banger/index.ts b/src/plugins/banger/index.ts new file mode 100644 index 0000000..68163cb --- /dev/null +++ b/src/plugins/banger/index.ts @@ -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 { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +export default definePlugin({ + name: "BANger", + description: "Replaces the GIF in the ban dialogue with a custom one.", + authors: [Devs.Xinto, Devs.Glitch], + patches: [ + { + find: "BAN_CONFIRM_TITLE.", + replacement: { + match: /src:\w\(\d+\)/g, + replace: "src: Vencord.Settings.plugins.BANger.source" + } + } + ], + options: { + source: { + description: "Source to replace ban GIF with (Video or Gif)", + type: OptionType.STRING, + default: "https://i.imgur.com/wp5q52C.mp4", + restartNeeded: true, + } + } +}); diff --git a/src/plugins/betterGifAltText.ts b/src/plugins/betterGifAltText.ts deleted file mode 100644 index 4dd30f2..0000000 --- a/src/plugins/betterGifAltText.ts +++ /dev/null @@ -1,69 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "BetterGifAltText", - authors: [Devs.Ven], - description: - "Change GIF alt text from simply being 'GIF' to containing the gif tags / filename", - patches: [ - { - find: "onCloseImage=", - replacement: { - match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/, - replace: - "$self.altify(e);$1", - }, - }, - { - find: ".embedGallerySide", - replacement: { - match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/, - replace: - "?($1.alt='GIF',$self.altify($1))", - }, - }, - ], - - altify(props: any) { - if (props.alt !== "GIF") return props.alt; - - let url: string = props.original || props.src; - try { - url = decodeURI(url); - } catch { } - - let name = url - .slice(url.lastIndexOf("/") + 1) - .replace(/\d/g, "") // strip numbers - .replace(/.gif$/, "") // strip extension - .split(/[,\-_ ]+/g) - .slice(0, 20) - .join(" "); - if (name.length > 300) { - name = name.slice(0, 300) + "..."; - } - - if (name) props.alt += ` - ${name}`; - - return props.alt; - }, -}); diff --git a/src/plugins/betterGifAltText/index.ts b/src/plugins/betterGifAltText/index.ts new file mode 100644 index 0000000..4dd30f2 --- /dev/null +++ b/src/plugins/betterGifAltText/index.ts @@ -0,0 +1,69 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "BetterGifAltText", + authors: [Devs.Ven], + description: + "Change GIF alt text from simply being 'GIF' to containing the gif tags / filename", + patches: [ + { + find: "onCloseImage=", + replacement: { + match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/, + replace: + "$self.altify(e);$1", + }, + }, + { + find: ".embedGallerySide", + replacement: { + match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/, + replace: + "?($1.alt='GIF',$self.altify($1))", + }, + }, + ], + + altify(props: any) { + if (props.alt !== "GIF") return props.alt; + + let url: string = props.original || props.src; + try { + url = decodeURI(url); + } catch { } + + let name = url + .slice(url.lastIndexOf("/") + 1) + .replace(/\d/g, "") // strip numbers + .replace(/.gif$/, "") // strip extension + .split(/[,\-_ ]+/g) + .slice(0, 20) + .join(" "); + if (name.length > 300) { + name = name.slice(0, 300) + "..."; + } + + if (name) props.alt += ` - ${name}`; + + return props.alt; + }, +}); diff --git a/src/plugins/betterNotes.ts b/src/plugins/betterNotes.ts deleted file mode 100644 index d9c5b45..0000000 --- a/src/plugins/betterNotes.ts +++ /dev/null @@ -1,60 +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"; - -export default definePlugin({ - name: "BetterNotesBox", - description: "Hide notes or disable spellcheck (Configure in settings!!)", - authors: [Devs.Ven], - - patches: [ - { - find: "hideNote:", - all: true, - predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide, - replacement: { - match: /hideNote:.+?(?=[,}])/g, - replace: "hideNote:true", - } - }, { - find: "Messages.NOTE_PLACEHOLDER", - replacement: { - match: /\.NOTE_PLACEHOLDER,/, - replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck," - } - } - ], - - options: { - hide: { - type: OptionType.BOOLEAN, - description: "Hide notes", - default: false, - restartNeeded: true - }, - noSpellCheck: { - type: OptionType.BOOLEAN, - description: "Disable spellcheck in notes", - disabled: () => Settings.plugins.BetterNotesBox.hide, - default: false - } - } -}); diff --git a/src/plugins/betterNotes/index.ts b/src/plugins/betterNotes/index.ts new file mode 100644 index 0000000..d9c5b45 --- /dev/null +++ b/src/plugins/betterNotes/index.ts @@ -0,0 +1,60 @@ +/* + * 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"; + +export default definePlugin({ + name: "BetterNotesBox", + description: "Hide notes or disable spellcheck (Configure in settings!!)", + authors: [Devs.Ven], + + patches: [ + { + find: "hideNote:", + all: true, + predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide, + replacement: { + match: /hideNote:.+?(?=[,}])/g, + replace: "hideNote:true", + } + }, { + find: "Messages.NOTE_PLACEHOLDER", + replacement: { + match: /\.NOTE_PLACEHOLDER,/, + replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck," + } + } + ], + + options: { + hide: { + type: OptionType.BOOLEAN, + description: "Hide notes", + default: false, + restartNeeded: true + }, + noSpellCheck: { + type: OptionType.BOOLEAN, + description: "Disable spellcheck in notes", + disabled: () => Settings.plugins.BetterNotesBox.hide, + default: false + } + } +}); diff --git a/src/plugins/betterRoleDot.ts b/src/plugins/betterRoleDot.ts deleted file mode 100644 index e8f69b8..0000000 --- a/src/plugins/betterRoleDot.ts +++ /dev/null @@ -1,93 +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"; -import { Clipboard, Toasts } from "@webpack/common"; - -export default definePlugin({ - name: "BetterRoleDot", - authors: [Devs.Ven, Devs.AutumnVN], - description: - "Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously", - - patches: [ - { - find: ".dotBorderBase", - replacement: { - match: /,viewBox:"0 0 20 20"/, - replace: "$&,onClick:()=>$self.copyToClipBoard(arguments[0].color),style:{cursor:'pointer'}", - }, - }, - { - find: '"dot"===', - all: true, - predicate: () => Settings.plugins.BetterRoleDot.bothStyles, - replacement: { - match: /"(?:username|dot)"===\i(?!\.\i)/g, - replace: "true", - }, - }, - - { - find: ".ADD_ROLE_A11Y_LABEL", - predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles, - replacement: { - match: /"dot"===\i/, - replace: "true" - } - }, - { - find: ".roleVerifiedIcon", - predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles, - replacement: { - match: /"dot"===\i/, - replace: "true" - } - } - ], - - options: { - bothStyles: { - type: OptionType.BOOLEAN, - description: "Show both role dot and coloured names", - restartNeeded: true, - default: false, - }, - copyRoleColorInProfilePopout: { - type: OptionType.BOOLEAN, - description: "Allow click on role dot in profile popout to copy role color", - restartNeeded: true, - default: false - } - }, - - copyToClipBoard(color: string) { - Clipboard.copy(color); - Toasts.show({ - message: "Copied to Clipboard!", - type: Toasts.Type.SUCCESS, - id: Toasts.genId(), - options: { - duration: 1000, - position: Toasts.Position.BOTTOM - } - }); - }, -}); diff --git a/src/plugins/betterRoleDot/index.ts b/src/plugins/betterRoleDot/index.ts new file mode 100644 index 0000000..e8f69b8 --- /dev/null +++ b/src/plugins/betterRoleDot/index.ts @@ -0,0 +1,93 @@ +/* + * 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"; +import { Clipboard, Toasts } from "@webpack/common"; + +export default definePlugin({ + name: "BetterRoleDot", + authors: [Devs.Ven, Devs.AutumnVN], + description: + "Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously", + + patches: [ + { + find: ".dotBorderBase", + replacement: { + match: /,viewBox:"0 0 20 20"/, + replace: "$&,onClick:()=>$self.copyToClipBoard(arguments[0].color),style:{cursor:'pointer'}", + }, + }, + { + find: '"dot"===', + all: true, + predicate: () => Settings.plugins.BetterRoleDot.bothStyles, + replacement: { + match: /"(?:username|dot)"===\i(?!\.\i)/g, + replace: "true", + }, + }, + + { + find: ".ADD_ROLE_A11Y_LABEL", + predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles, + replacement: { + match: /"dot"===\i/, + replace: "true" + } + }, + { + find: ".roleVerifiedIcon", + predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles, + replacement: { + match: /"dot"===\i/, + replace: "true" + } + } + ], + + options: { + bothStyles: { + type: OptionType.BOOLEAN, + description: "Show both role dot and coloured names", + restartNeeded: true, + default: false, + }, + copyRoleColorInProfilePopout: { + type: OptionType.BOOLEAN, + description: "Allow click on role dot in profile popout to copy role color", + restartNeeded: true, + default: false + } + }, + + copyToClipBoard(color: string) { + Clipboard.copy(color); + Toasts.show({ + message: "Copied to Clipboard!", + type: Toasts.Type.SUCCESS, + id: Toasts.genId(), + options: { + duration: 1000, + position: Toasts.Position.BOTTOM + } + }); + }, +}); diff --git a/src/plugins/betterUploadButton.ts b/src/plugins/betterUploadButton.ts deleted file mode 100644 index 64a3785..0000000 --- a/src/plugins/betterUploadButton.ts +++ /dev/null @@ -1,38 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "BetterUploadButton", - authors: [Devs.obscurity, Devs.Ven], - description: "Upload with a single click, open menu with right click", - patches: [ - { - find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE", - replacement: { - // Discord merges multiple props here with Object.assign() - // This patch passes a third object to it with which we override onClick and onContextMenu - match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0)\},(.{1,3})\)/, - replace: (m, onDblClick, otherProps) => - `${m.slice(0, -1)},{onClick:${onDblClick},onContextMenu:${otherProps}.onClick})`, - }, - }, - ], -}); diff --git a/src/plugins/betterUploadButton/index.ts b/src/plugins/betterUploadButton/index.ts new file mode 100644 index 0000000..64a3785 --- /dev/null +++ b/src/plugins/betterUploadButton/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "BetterUploadButton", + authors: [Devs.obscurity, Devs.Ven], + description: "Upload with a single click, open menu with right click", + patches: [ + { + find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE", + replacement: { + // Discord merges multiple props here with Object.assign() + // This patch passes a third object to it with which we override onClick and onContextMenu + match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0)\},(.{1,3})\)/, + replace: (m, onDblClick, otherProps) => + `${m.slice(0, -1)},{onClick:${onDblClick},onContextMenu:${otherProps}.onClick})`, + }, + }, + ], +}); diff --git a/src/plugins/blurNsfw.ts b/src/plugins/blurNsfw.ts deleted file mode 100644 index 54b1e49..0000000 --- a/src/plugins/blurNsfw.ts +++ /dev/null @@ -1,77 +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"; - -let style: HTMLStyleElement; - -function setCss() { - style.textContent = ` - .vc-nsfw-img [class^=imageWrapper] img, - .vc-nsfw-img [class^=wrapperPaused] video { - filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px); - transition: filter 0.2s; - } - .vc-nsfw-img [class^=imageWrapper]:hover img, - .vc-nsfw-img [class^=wrapperPaused]:hover video { - filter: unset; - } - `; -} - -export default definePlugin({ - name: "BlurNSFW", - description: "Blur attachments in NSFW channels until hovered", - authors: [Devs.Ven], - - patches: [ - { - find: ".embedWrapper,embed", - replacement: [{ - match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\.embedWrapper)/g, - replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')" - }, { - match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g, - replace: "$1,vcProps=$2$3+(vcProps.nsfw?' vc-nsfw-img':'')" - }] - } - ], - - options: { - blurAmount: { - type: OptionType.NUMBER, - description: "Blur Amount", - default: 10, - onChange: setCss - } - }, - - start() { - style = document.createElement("style"); - style.id = "VcBlurNsfw"; - document.head.appendChild(style); - - setCss(); - }, - - stop() { - style?.remove(); - } -}); diff --git a/src/plugins/blurNsfw/index.ts b/src/plugins/blurNsfw/index.ts new file mode 100644 index 0000000..54b1e49 --- /dev/null +++ b/src/plugins/blurNsfw/index.ts @@ -0,0 +1,77 @@ +/* + * 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"; + +let style: HTMLStyleElement; + +function setCss() { + style.textContent = ` + .vc-nsfw-img [class^=imageWrapper] img, + .vc-nsfw-img [class^=wrapperPaused] video { + filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px); + transition: filter 0.2s; + } + .vc-nsfw-img [class^=imageWrapper]:hover img, + .vc-nsfw-img [class^=wrapperPaused]:hover video { + filter: unset; + } + `; +} + +export default definePlugin({ + name: "BlurNSFW", + description: "Blur attachments in NSFW channels until hovered", + authors: [Devs.Ven], + + patches: [ + { + find: ".embedWrapper,embed", + replacement: [{ + match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\.embedWrapper)/g, + replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')" + }, { + match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g, + replace: "$1,vcProps=$2$3+(vcProps.nsfw?' vc-nsfw-img':'')" + }] + } + ], + + options: { + blurAmount: { + type: OptionType.NUMBER, + description: "Blur Amount", + default: 10, + onChange: setCss + } + }, + + start() { + style = document.createElement("style"); + style.id = "VcBlurNsfw"; + document.head.appendChild(style); + + setCss(); + }, + + stop() { + style?.remove(); + } +}); diff --git a/src/plugins/callTimer.tsx b/src/plugins/callTimer.tsx deleted file mode 100644 index 2e0aa96..0000000 --- a/src/plugins/callTimer.tsx +++ /dev/null @@ -1,95 +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 ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { useTimer } from "@utils/react"; -import definePlugin, { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; - -function formatDuration(ms: number) { - // here be dragons (moment fucking sucks) - const human = Settings.plugins.CallTimer.format === "human"; - - const format = (n: number) => human ? n : n.toString().padStart(2, "0"); - const unit = (s: string) => human ? s : ""; - const delim = human ? " " : ":"; - - // thx copilot - const d = Math.floor(ms / 86400000); - const h = Math.floor((ms % 86400000) / 3600000); - const m = Math.floor(((ms % 86400000) % 3600000) / 60000); - const s = Math.floor((((ms % 86400000) % 3600000) % 60000) / 1000); - - let res = ""; - if (d) res += `${d}d `; - if (h || res) res += `${format(h)}${unit("h")}${delim}`; - if (m || res || !human) res += `${format(m)}${unit("m")}${delim}`; - res += `${format(s)}${unit("s")}`; - - return res; -} - -export default definePlugin({ - name: "CallTimer", - description: "Adds a timer to vcs", - authors: [Devs.Ven], - - startTime: 0, - interval: void 0 as NodeJS.Timeout | undefined, - - options: { - format: { - type: OptionType.SELECT, - description: "The timer format. This can be any valid moment.js format", - options: [ - { - label: "30d 23:00:42", - value: "stopwatch", - default: true - }, - { - label: "30d 23h 00m 42s", - value: "human" - } - ] - } - }, - - patches: [{ - find: ".renderConnectionStatus=", - replacement: { - match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/, - replace: "[$&, $self.renderTimer(this.props.channel.id)]" - } - }], - renderTimer(channelId: string) { - return - - ; - }, - - Timer({ channelId }: { channelId: string; }) { - const time = useTimer({ - deps: [channelId] - }); - - return

Connected for {formatDuration(time)}

; - } -}); diff --git a/src/plugins/callTimer/index.tsx b/src/plugins/callTimer/index.tsx new file mode 100644 index 0000000..2e0aa96 --- /dev/null +++ b/src/plugins/callTimer/index.tsx @@ -0,0 +1,95 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Settings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { useTimer } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { React } from "@webpack/common"; + +function formatDuration(ms: number) { + // here be dragons (moment fucking sucks) + const human = Settings.plugins.CallTimer.format === "human"; + + const format = (n: number) => human ? n : n.toString().padStart(2, "0"); + const unit = (s: string) => human ? s : ""; + const delim = human ? " " : ":"; + + // thx copilot + const d = Math.floor(ms / 86400000); + const h = Math.floor((ms % 86400000) / 3600000); + const m = Math.floor(((ms % 86400000) % 3600000) / 60000); + const s = Math.floor((((ms % 86400000) % 3600000) % 60000) / 1000); + + let res = ""; + if (d) res += `${d}d `; + if (h || res) res += `${format(h)}${unit("h")}${delim}`; + if (m || res || !human) res += `${format(m)}${unit("m")}${delim}`; + res += `${format(s)}${unit("s")}`; + + return res; +} + +export default definePlugin({ + name: "CallTimer", + description: "Adds a timer to vcs", + authors: [Devs.Ven], + + startTime: 0, + interval: void 0 as NodeJS.Timeout | undefined, + + options: { + format: { + type: OptionType.SELECT, + description: "The timer format. This can be any valid moment.js format", + options: [ + { + label: "30d 23:00:42", + value: "stopwatch", + default: true + }, + { + label: "30d 23h 00m 42s", + value: "human" + } + ] + } + }, + + patches: [{ + find: ".renderConnectionStatus=", + replacement: { + match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/, + replace: "[$&, $self.renderTimer(this.props.channel.id)]" + } + }], + renderTimer(channelId: string) { + return + + ; + }, + + Timer({ channelId }: { channelId: string; }) { + const time = useTimer({ + deps: [channelId] + }); + + return

Connected for {formatDuration(time)}

; + } +}); diff --git a/src/plugins/colorSighted.ts b/src/plugins/colorSighted.ts deleted file mode 100644 index d2fb0d6..0000000 --- a/src/plugins/colorSighted.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "ColorSighted", - description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord", - authors: [Devs.lewisakura], - patches: [ - { - find: "Masks.STATUS_ONLINE", - replacement: { - match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g, - replace: "Masks.STATUS_ONLINE" - } - }, - { - find: ".AVATAR_STATUS_MOBILE_16;", - replacement: { - match: /(\.fromIsMobile,.+?)\i.status/, - replace: (_, rest) => `${rest}"online"` - } - } - ] -}); diff --git a/src/plugins/colorSighted/index.ts b/src/plugins/colorSighted/index.ts new file mode 100644 index 0000000..d2fb0d6 --- /dev/null +++ b/src/plugins/colorSighted/index.ts @@ -0,0 +1,42 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "ColorSighted", + description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord", + authors: [Devs.lewisakura], + patches: [ + { + find: "Masks.STATUS_ONLINE", + replacement: { + match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g, + replace: "Masks.STATUS_ONLINE" + } + }, + { + find: ".AVATAR_STATUS_MOBILE_16;", + replacement: { + match: /(\.fromIsMobile,.+?)\i.status/, + replace: (_, rest) => `${rest}"online"` + } + } + ] +}); diff --git a/src/plugins/consoleShortcuts.ts b/src/plugins/consoleShortcuts.ts deleted file mode 100644 index 1c23d60..0000000 --- a/src/plugins/consoleShortcuts.ts +++ /dev/null @@ -1,112 +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 { Devs } from "@utils/constants"; -import { relaunch } from "@utils/native"; -import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches"; -import definePlugin from "@utils/types"; -import * as Webpack from "@webpack"; -import { extract, filters, findAll, search } from "@webpack"; -import { React, ReactDOM } from "@webpack/common"; -import type { ComponentType } from "react"; - -const WEB_ONLY = (f: string) => () => { - throw new Error(`'${f}' is Discord Desktop only.`); -}; - -export default definePlugin({ - name: "ConsoleShortcuts", - description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.", - authors: [Devs.Ven], - - getShortcuts() { - function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) { - const cache = new Map(); - - return function (...filterProps: unknown[]) { - const cacheKey = String(filterProps); - if (cache.has(cacheKey)) return cache.get(cacheKey); - - const matches = findAll(filterFactory(...filterProps)); - - const result = (() => { - switch (matches.length) { - case 0: return null; - case 1: return matches[0]; - default: - const uniqueMatches = [...new Set(matches)]; - if (uniqueMatches.length > 1) - console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches); - - return matches[0]; - } - })(); - if (result && cacheKey) cache.set(cacheKey, result); - return result; - }; - } - - let fakeRenderWin: WeakRef | undefined; - return { - wp: Vencord.Webpack, - wpc: Webpack.wreq.c, - wreq: Webpack.wreq, - wpsearch: search, - wpex: extract, - wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!), - find: newFindWrapper(f => f), - findAll, - findByProps: newFindWrapper(filters.byProps), - findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), - findByCode: newFindWrapper(filters.byCode), - findAllByCode: (code: string) => findAll(filters.byCode(code)), - findStore: newFindWrapper(filters.byStoreName), - PluginsApi: Vencord.Plugins, - plugins: Vencord.Plugins.plugins, - React, - Settings: Vencord.Settings, - Api: Vencord.Api, - reload: () => location.reload(), - restart: IS_WEB ? WEB_ONLY("restart") : relaunch, - canonicalizeMatch, - canonicalizeReplace, - canonicalizeReplacement, - fakeRender: (component: ComponentType, props: any) => { - const prevWin = fakeRenderWin?.deref(); - const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!; - fakeRenderWin = new WeakRef(win); - win.focus(); - - ReactDOM.render(React.createElement(component, props), win.document.body); - } - }; - }, - - start() { - const shortcuts = this.getShortcuts(); - window.shortcutList = shortcuts; - for (const [key, val] of Object.entries(shortcuts)) - window[key] = val; - }, - - stop() { - delete window.shortcutList; - for (const key in this.getShortcuts()) - delete window[key]; - } -}); diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts new file mode 100644 index 0000000..1c23d60 --- /dev/null +++ b/src/plugins/consoleShortcuts/index.ts @@ -0,0 +1,112 @@ +/* + * 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 { Devs } from "@utils/constants"; +import { relaunch } from "@utils/native"; +import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches"; +import definePlugin from "@utils/types"; +import * as Webpack from "@webpack"; +import { extract, filters, findAll, search } from "@webpack"; +import { React, ReactDOM } from "@webpack/common"; +import type { ComponentType } from "react"; + +const WEB_ONLY = (f: string) => () => { + throw new Error(`'${f}' is Discord Desktop only.`); +}; + +export default definePlugin({ + name: "ConsoleShortcuts", + description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.", + authors: [Devs.Ven], + + getShortcuts() { + function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) { + const cache = new Map(); + + return function (...filterProps: unknown[]) { + const cacheKey = String(filterProps); + if (cache.has(cacheKey)) return cache.get(cacheKey); + + const matches = findAll(filterFactory(...filterProps)); + + const result = (() => { + switch (matches.length) { + case 0: return null; + case 1: return matches[0]; + default: + const uniqueMatches = [...new Set(matches)]; + if (uniqueMatches.length > 1) + console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches); + + return matches[0]; + } + })(); + if (result && cacheKey) cache.set(cacheKey, result); + return result; + }; + } + + let fakeRenderWin: WeakRef | undefined; + return { + wp: Vencord.Webpack, + wpc: Webpack.wreq.c, + wreq: Webpack.wreq, + wpsearch: search, + wpex: extract, + wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!), + find: newFindWrapper(f => f), + findAll, + findByProps: newFindWrapper(filters.byProps), + findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), + findByCode: newFindWrapper(filters.byCode), + findAllByCode: (code: string) => findAll(filters.byCode(code)), + findStore: newFindWrapper(filters.byStoreName), + PluginsApi: Vencord.Plugins, + plugins: Vencord.Plugins.plugins, + React, + Settings: Vencord.Settings, + Api: Vencord.Api, + reload: () => location.reload(), + restart: IS_WEB ? WEB_ONLY("restart") : relaunch, + canonicalizeMatch, + canonicalizeReplace, + canonicalizeReplacement, + fakeRender: (component: ComponentType, props: any) => { + const prevWin = fakeRenderWin?.deref(); + const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!; + fakeRenderWin = new WeakRef(win); + win.focus(); + + ReactDOM.render(React.createElement(component, props), win.document.body); + } + }; + }, + + start() { + const shortcuts = this.getShortcuts(); + window.shortcutList = shortcuts; + for (const [key, val] of Object.entries(shortcuts)) + window[key] = val; + }, + + stop() { + delete window.shortcutList; + for (const key in this.getShortcuts()) + delete window[key]; + } +}); diff --git a/src/plugins/copyUserURLs.tsx b/src/plugins/copyUserURLs.tsx deleted file mode 100644 index e3c336f..0000000 --- a/src/plugins/copyUserURLs.tsx +++ /dev/null @@ -1,55 +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 { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; -import { LinkIcon } from "@components/Icons"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { Clipboard, Menu } from "@webpack/common"; -import type { Channel, User } from "discord-types/general"; - -interface UserContextProps { - channel: Channel; - guildId?: string; - user: User; -} - -const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => { - children.push( - Clipboard.copy(``)} - icon={LinkIcon} - /> - ); -}; - -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); - }, -}); diff --git a/src/plugins/copyUserURLs/index.tsx b/src/plugins/copyUserURLs/index.tsx new file mode 100644 index 0000000..e3c336f --- /dev/null +++ b/src/plugins/copyUserURLs/index.tsx @@ -0,0 +1,55 @@ +/* + * 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, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { LinkIcon } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Clipboard, Menu } from "@webpack/common"; +import type { Channel, User } from "discord-types/general"; + +interface UserContextProps { + channel: Channel; + guildId?: string; + user: User; +} + +const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => { + children.push( + Clipboard.copy(``)} + icon={LinkIcon} + /> + ); +}; + +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); + }, +}); diff --git a/src/plugins/crashHandler.ts b/src/plugins/crashHandler.ts deleted file mode 100644 index a1ba01c..0000000 --- a/src/plugins/crashHandler.ts +++ /dev/null @@ -1,158 +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 { showNotification } from "@api/Notifications"; -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import { closeAllModals } from "@utils/modal"; -import definePlugin, { OptionType } from "@utils/types"; -import { maybePromptToUpdate } from "@utils/updater"; -import { findByPropsLazy } from "@webpack"; -import { FluxDispatcher, NavigationRouter } from "@webpack/common"; -import type { ReactElement } from "react"; - -const CrashHandlerLogger = new Logger("CrashHandler"); -const ModalStack = findByPropsLazy("pushLazy", "popAll"); - -const settings = definePluginSettings({ - attemptToPreventCrashes: { - type: OptionType.BOOLEAN, - description: "Whether to attempt to prevent Discord crashes.", - default: true - }, - attemptToNavigateToHome: { - type: OptionType.BOOLEAN, - description: "Whether to attempt to navigate to the home when preventing Discord crashes.", - default: false - } -}); - -let crashCount: number = 0; -let lastCrashTimestamp: number = 0; -let shouldAttemptNextHandle = false; - -export default definePlugin({ - name: "CrashHandler", - description: "Utility plugin for handling and possibly recovering from Crashes without a restart", - authors: [Devs.Nuckyz], - enabledByDefault: true, - - settings, - - patches: [ - { - find: ".Messages.ERRORS_UNEXPECTED_CRASH", - replacement: { - match: /(?=this\.setState\()/, - replace: "$self.handleCrash(this)||" - } - } - ], - - handleCrash(_this: ReactElement & { forceUpdate: () => void; }) { - if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true; - - 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 false; - } catch (err) { - CrashHandlerLogger.error("Failed to handle crash", err); - return false; - } finally { - lastCrashTimestamp = Date.now(); - } - }, - - handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) { - if (Date.now() - lastCrashTimestamp >= 1_000) { - try { - showNotification({ - color: "#eed202", - title: "Discord has crashed!", - body: "Attempting to recover...", - noPersist: true, - }); - } catch { } - } - - try { - FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" }); - } catch (err) { - CrashHandlerLogger.debug("Failed to close open context menu.", err); - } - try { - ModalStack?.popAll(); - } catch (err) { - CrashHandlerLogger.debug("Failed to close old modals.", err); - } - try { - closeAllModals(); - } catch (err) { - CrashHandlerLogger.debug("Failed to close all open modals.", err); - } - try { - FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" }); - } catch (err) { - CrashHandlerLogger.debug("Failed to close user popout.", err); - } - try { - FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); - } catch (err) { - CrashHandlerLogger.debug("Failed to pop all layers.", err); - } - if (settings.store.attemptToNavigateToHome) { - try { - NavigationRouter.transitionTo("/channels/@me"); - } catch (err) { - CrashHandlerLogger.debug("Failed to navigate to home", err); - } - } - - try { - shouldAttemptNextHandle = true; - _this.forceUpdate(); - } catch (err) { - CrashHandlerLogger.debug("Failed to update crash handler component.", err); - } - } -}); diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts new file mode 100644 index 0000000..a1ba01c --- /dev/null +++ b/src/plugins/crashHandler/index.ts @@ -0,0 +1,158 @@ +/* + * 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 { showNotification } from "@api/Notifications"; +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import { closeAllModals } from "@utils/modal"; +import definePlugin, { OptionType } from "@utils/types"; +import { maybePromptToUpdate } from "@utils/updater"; +import { findByPropsLazy } from "@webpack"; +import { FluxDispatcher, NavigationRouter } from "@webpack/common"; +import type { ReactElement } from "react"; + +const CrashHandlerLogger = new Logger("CrashHandler"); +const ModalStack = findByPropsLazy("pushLazy", "popAll"); + +const settings = definePluginSettings({ + attemptToPreventCrashes: { + type: OptionType.BOOLEAN, + description: "Whether to attempt to prevent Discord crashes.", + default: true + }, + attemptToNavigateToHome: { + type: OptionType.BOOLEAN, + description: "Whether to attempt to navigate to the home when preventing Discord crashes.", + default: false + } +}); + +let crashCount: number = 0; +let lastCrashTimestamp: number = 0; +let shouldAttemptNextHandle = false; + +export default definePlugin({ + name: "CrashHandler", + description: "Utility plugin for handling and possibly recovering from Crashes without a restart", + authors: [Devs.Nuckyz], + enabledByDefault: true, + + settings, + + patches: [ + { + find: ".Messages.ERRORS_UNEXPECTED_CRASH", + replacement: { + match: /(?=this\.setState\()/, + replace: "$self.handleCrash(this)||" + } + } + ], + + handleCrash(_this: ReactElement & { forceUpdate: () => void; }) { + if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true; + + 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 false; + } catch (err) { + CrashHandlerLogger.error("Failed to handle crash", err); + return false; + } finally { + lastCrashTimestamp = Date.now(); + } + }, + + handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) { + if (Date.now() - lastCrashTimestamp >= 1_000) { + try { + showNotification({ + color: "#eed202", + title: "Discord has crashed!", + body: "Attempting to recover...", + noPersist: true, + }); + } catch { } + } + + try { + FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" }); + } catch (err) { + CrashHandlerLogger.debug("Failed to close open context menu.", err); + } + try { + ModalStack?.popAll(); + } catch (err) { + CrashHandlerLogger.debug("Failed to close old modals.", err); + } + try { + closeAllModals(); + } catch (err) { + CrashHandlerLogger.debug("Failed to close all open modals.", err); + } + try { + FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" }); + } catch (err) { + CrashHandlerLogger.debug("Failed to close user popout.", err); + } + try { + FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + } catch (err) { + CrashHandlerLogger.debug("Failed to pop all layers.", err); + } + if (settings.store.attemptToNavigateToHome) { + try { + NavigationRouter.transitionTo("/channels/@me"); + } catch (err) { + CrashHandlerLogger.debug("Failed to navigate to home", err); + } + } + + try { + shouldAttemptNextHandle = true; + _this.forceUpdate(); + } catch (err) { + CrashHandlerLogger.debug("Failed to update crash handler component.", err); + } + } +}); diff --git a/src/plugins/customRPC.tsx b/src/plugins/customRPC.tsx deleted file mode 100644 index a58e542..0000000 --- a/src/plugins/customRPC.tsx +++ /dev/null @@ -1,425 +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 } from "@api/Settings"; -import { Link } from "@components/Link"; -import { Devs } from "@utils/constants"; -import { isTruthy } from "@utils/guards"; -import { useAwaiter } from "@utils/react"; -import definePlugin, { OptionType } from "@utils/types"; -import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; -import { FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; - -const ActivityComponent = findByCodeLazy("onOpenGameProfile"); -const ActivityClassName = findByPropsLazy("activity", "buttonColor"); -const Colors = findByPropsLazy("profileColors"); - -const assetManager = mapMangledModuleLazy( - "getAssetImage: size must === [number, number] for Twitch", - { - getAsset: filters.byCode("apply("), - } -); - -async function getApplicationAsset(key: string): Promise { - if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, ""); - return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0]; -} - -interface ActivityAssets { - large_image?: string; - large_text?: string; - small_image?: string; - small_text?: string; -} - -interface Activity { - state?: string; - details?: string; - timestamps?: { - start?: number; - end?: number; - }; - assets?: ActivityAssets; - buttons?: Array; - name: string; - application_id: string; - metadata?: { - button_urls?: Array; - }; - type: ActivityType; - url?: string; - flags: number; -} - -const enum ActivityType { - PLAYING = 0, - STREAMING = 1, - LISTENING = 2, - WATCHING = 3, - COMPETING = 5 -} - -const enum TimestampMode { - NONE, - NOW, - TIME, - CUSTOM, -} - -const settings = definePluginSettings({ - appID: { - type: OptionType.STRING, - description: "Application ID (required)", - onChange: onChange, - isValid: (value: string) => { - if (!value) return "Application ID is required."; - if (value && !/^\d+$/.test(value)) return "Application ID must be a number."; - return true; - } - }, - appName: { - type: OptionType.STRING, - description: "Application name (required)", - onChange: onChange, - isValid: (value: string) => { - if (!value) return "Application name is required."; - if (value.length > 128) return "Application name must be not longer than 128 characters."; - return true; - } - }, - details: { - type: OptionType.STRING, - description: "Details (line 1)", - onChange: onChange, - isValid: (value: string) => { - if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters."; - return true; - } - }, - state: { - type: OptionType.STRING, - description: "State (line 2)", - onChange: onChange, - isValid: (value: string) => { - if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters."; - return true; - } - }, - type: { - type: OptionType.SELECT, - description: "Activity type", - onChange: onChange, - options: [ - { - label: "Playing", - value: ActivityType.PLAYING, - default: true - }, - { - label: "Streaming", - value: ActivityType.STREAMING - }, - { - label: "Listening", - value: ActivityType.LISTENING - }, - { - label: "Watching", - value: ActivityType.WATCHING - }, - { - label: "Competing", - value: ActivityType.COMPETING - } - ] - }, - streamLink: { - type: OptionType.STRING, - description: "Twitch.tv or Youtube.com link (only for Streaming activity type)", - onChange: onChange, - disabled: isStreamLinkDisabled, - isValid: isStreamLinkValid - }, - timestampMode: { - type: OptionType.SELECT, - description: "Timestamp mode", - onChange: onChange, - options: [ - { - label: "None", - value: TimestampMode.NONE, - default: true - }, - { - label: "Since discord open", - value: TimestampMode.NOW - }, - { - label: "Same as your current time", - value: TimestampMode.TIME - }, - { - label: "Custom", - value: TimestampMode.CUSTOM - } - ] - }, - startTime: { - type: OptionType.NUMBER, - description: "Start timestamp (only for custom timestamp mode)", - onChange: onChange, - disabled: isTimestampDisabled, - isValid: (value: number) => { - if (value && value < 0) return "Start timestamp must be greater than 0."; - return true; - } - }, - endTime: { - type: OptionType.NUMBER, - description: "End timestamp (only for custom timestamp mode)", - onChange: onChange, - disabled: isTimestampDisabled, - isValid: (value: number) => { - if (value && value < 0) return "End timestamp must be greater than 0."; - return true; - } - }, - imageBig: { - type: OptionType.STRING, - description: "Big image key/link", - onChange: onChange, - isValid: isImageKeyValid - }, - imageBigTooltip: { - type: OptionType.STRING, - description: "Big image tooltip", - onChange: onChange, - isValid: (value: string) => { - if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters."; - return true; - } - }, - imageSmall: { - type: OptionType.STRING, - description: "Small image key/link", - onChange: onChange, - isValid: isImageKeyValid - }, - imageSmallTooltip: { - type: OptionType.STRING, - description: "Small image tooltip", - onChange: onChange, - isValid: (value: string) => { - if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters."; - return true; - } - }, - buttonOneText: { - type: OptionType.STRING, - description: "Button 1 text", - onChange: onChange, - isValid: (value: string) => { - if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters."; - return true; - } - }, - buttonOneURL: { - type: OptionType.STRING, - description: "Button 1 URL", - onChange: onChange - }, - buttonTwoText: { - type: OptionType.STRING, - description: "Button 2 text", - onChange: onChange, - isValid: (value: string) => { - if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters."; - return true; - } - }, - buttonTwoURL: { - type: OptionType.STRING, - description: "Button 2 URL", - onChange: onChange - } -}); - -function onChange() { - setRpc(true); - if (Settings.plugins.CustomRPC.enabled) setRpc(); -} - -function isStreamLinkDisabled() { - return settings.store.type !== ActivityType.STREAMING; -} - -function isStreamLinkValid(value: string) { - if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL."; - return true; -} - -function isTimestampDisabled() { - return settings.store.timestampMode !== TimestampMode.CUSTOM; -} - -function isImageKeyValid(value: string) { - if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)"; - if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)"; - return true; -} - -async function createActivity(): Promise { - const { - appID, - appName, - details, - state, - type, - streamLink, - startTime, - endTime, - imageBig, - imageBigTooltip, - imageSmall, - imageSmallTooltip, - buttonOneText, - buttonOneURL, - buttonTwoText, - buttonTwoURL - } = settings.store; - - if (!appName) return; - - const activity: Activity = { - application_id: appID || "0", - name: appName, - state, - details, - type, - flags: 1 << 0, - }; - - if (type === ActivityType.STREAMING) activity.url = streamLink; - - switch (settings.store.timestampMode) { - case TimestampMode.NOW: - activity.timestamps = { - start: Math.floor(Date.now() / 1000) - }; - break; - case TimestampMode.TIME: - activity.timestamps = { - start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds() - }; - break; - case TimestampMode.CUSTOM: - if (startTime || endTime) { - activity.timestamps = {}; - if (startTime) activity.timestamps.start = startTime; - if (endTime) activity.timestamps.end = endTime; - } - break; - case TimestampMode.NONE: - default: - break; - } - - if (buttonOneText) { - activity.buttons = [ - buttonOneText, - buttonTwoText - ].filter(isTruthy); - - activity.metadata = { - button_urls: [ - buttonOneURL, - buttonTwoURL - ].filter(isTruthy) - }; - } - - if (imageBig) { - activity.assets = { - large_image: await getApplicationAsset(imageBig), - large_text: imageBigTooltip || undefined - }; - } - - if (imageSmall) { - activity.assets = { - ...activity.assets, - small_image: await getApplicationAsset(imageSmall), - small_text: imageSmallTooltip || undefined - }; - } - - - for (const k in activity) { - if (k === "type") continue; - const v = activity[k]; - if (!v || v.length === 0) - delete activity[k]; - } - - return activity; -} - -async function setRpc(disable?: boolean) { - const activity: Activity | undefined = await createActivity(); - - FluxDispatcher.dispatch({ - type: "LOCAL_ACTIVITY_UPDATE", - activity: !disable ? activity : null, - socketId: "CustomRPC", - }); -} - -export default definePlugin({ - name: "CustomRPC", - description: "Allows you to set a custom rich presence.", - authors: [Devs.captain, Devs.AutumnVN], - start: setRpc, - stop: () => setRpc(true), - settings, - - settingsAboutComponent: () => { - const activity = useAwaiter(createActivity); - return ( - <> - - Go to Discord Deverloper Portal to create an application and - get the application ID. - - - Upload images in the Rich Presence tab to get the image keys. - - - If you want to use image link, download your image and reupload the image to Imgur and get the image link by right-clicking the image and select "Copy image address". - - -
- {activity[0] && } -
- - ); - } -}); diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx new file mode 100644 index 0000000..a58e542 --- /dev/null +++ b/src/plugins/customRPC/index.tsx @@ -0,0 +1,425 @@ +/* + * 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 } from "@api/Settings"; +import { Link } from "@components/Link"; +import { Devs } from "@utils/constants"; +import { isTruthy } from "@utils/guards"; +import { useAwaiter } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; +import { FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; + +const ActivityComponent = findByCodeLazy("onOpenGameProfile"); +const ActivityClassName = findByPropsLazy("activity", "buttonColor"); +const Colors = findByPropsLazy("profileColors"); + +const assetManager = mapMangledModuleLazy( + "getAssetImage: size must === [number, number] for Twitch", + { + getAsset: filters.byCode("apply("), + } +); + +async function getApplicationAsset(key: string): Promise { + if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, ""); + return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0]; +} + +interface ActivityAssets { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; +} + +interface Activity { + state?: string; + details?: string; + timestamps?: { + start?: number; + end?: number; + }; + assets?: ActivityAssets; + buttons?: Array; + name: string; + application_id: string; + metadata?: { + button_urls?: Array; + }; + type: ActivityType; + url?: string; + flags: number; +} + +const enum ActivityType { + PLAYING = 0, + STREAMING = 1, + LISTENING = 2, + WATCHING = 3, + COMPETING = 5 +} + +const enum TimestampMode { + NONE, + NOW, + TIME, + CUSTOM, +} + +const settings = definePluginSettings({ + appID: { + type: OptionType.STRING, + description: "Application ID (required)", + onChange: onChange, + isValid: (value: string) => { + if (!value) return "Application ID is required."; + if (value && !/^\d+$/.test(value)) return "Application ID must be a number."; + return true; + } + }, + appName: { + type: OptionType.STRING, + description: "Application name (required)", + onChange: onChange, + isValid: (value: string) => { + if (!value) return "Application name is required."; + if (value.length > 128) return "Application name must be not longer than 128 characters."; + return true; + } + }, + details: { + type: OptionType.STRING, + description: "Details (line 1)", + onChange: onChange, + isValid: (value: string) => { + if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters."; + return true; + } + }, + state: { + type: OptionType.STRING, + description: "State (line 2)", + onChange: onChange, + isValid: (value: string) => { + if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters."; + return true; + } + }, + type: { + type: OptionType.SELECT, + description: "Activity type", + onChange: onChange, + options: [ + { + label: "Playing", + value: ActivityType.PLAYING, + default: true + }, + { + label: "Streaming", + value: ActivityType.STREAMING + }, + { + label: "Listening", + value: ActivityType.LISTENING + }, + { + label: "Watching", + value: ActivityType.WATCHING + }, + { + label: "Competing", + value: ActivityType.COMPETING + } + ] + }, + streamLink: { + type: OptionType.STRING, + description: "Twitch.tv or Youtube.com link (only for Streaming activity type)", + onChange: onChange, + disabled: isStreamLinkDisabled, + isValid: isStreamLinkValid + }, + timestampMode: { + type: OptionType.SELECT, + description: "Timestamp mode", + onChange: onChange, + options: [ + { + label: "None", + value: TimestampMode.NONE, + default: true + }, + { + label: "Since discord open", + value: TimestampMode.NOW + }, + { + label: "Same as your current time", + value: TimestampMode.TIME + }, + { + label: "Custom", + value: TimestampMode.CUSTOM + } + ] + }, + startTime: { + type: OptionType.NUMBER, + description: "Start timestamp (only for custom timestamp mode)", + onChange: onChange, + disabled: isTimestampDisabled, + isValid: (value: number) => { + if (value && value < 0) return "Start timestamp must be greater than 0."; + return true; + } + }, + endTime: { + type: OptionType.NUMBER, + description: "End timestamp (only for custom timestamp mode)", + onChange: onChange, + disabled: isTimestampDisabled, + isValid: (value: number) => { + if (value && value < 0) return "End timestamp must be greater than 0."; + return true; + } + }, + imageBig: { + type: OptionType.STRING, + description: "Big image key/link", + onChange: onChange, + isValid: isImageKeyValid + }, + imageBigTooltip: { + type: OptionType.STRING, + description: "Big image tooltip", + onChange: onChange, + isValid: (value: string) => { + if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters."; + return true; + } + }, + imageSmall: { + type: OptionType.STRING, + description: "Small image key/link", + onChange: onChange, + isValid: isImageKeyValid + }, + imageSmallTooltip: { + type: OptionType.STRING, + description: "Small image tooltip", + onChange: onChange, + isValid: (value: string) => { + if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters."; + return true; + } + }, + buttonOneText: { + type: OptionType.STRING, + description: "Button 1 text", + onChange: onChange, + isValid: (value: string) => { + if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters."; + return true; + } + }, + buttonOneURL: { + type: OptionType.STRING, + description: "Button 1 URL", + onChange: onChange + }, + buttonTwoText: { + type: OptionType.STRING, + description: "Button 2 text", + onChange: onChange, + isValid: (value: string) => { + if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters."; + return true; + } + }, + buttonTwoURL: { + type: OptionType.STRING, + description: "Button 2 URL", + onChange: onChange + } +}); + +function onChange() { + setRpc(true); + if (Settings.plugins.CustomRPC.enabled) setRpc(); +} + +function isStreamLinkDisabled() { + return settings.store.type !== ActivityType.STREAMING; +} + +function isStreamLinkValid(value: string) { + if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL."; + return true; +} + +function isTimestampDisabled() { + return settings.store.timestampMode !== TimestampMode.CUSTOM; +} + +function isImageKeyValid(value: string) { + if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)"; + if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)"; + return true; +} + +async function createActivity(): Promise { + const { + appID, + appName, + details, + state, + type, + streamLink, + startTime, + endTime, + imageBig, + imageBigTooltip, + imageSmall, + imageSmallTooltip, + buttonOneText, + buttonOneURL, + buttonTwoText, + buttonTwoURL + } = settings.store; + + if (!appName) return; + + const activity: Activity = { + application_id: appID || "0", + name: appName, + state, + details, + type, + flags: 1 << 0, + }; + + if (type === ActivityType.STREAMING) activity.url = streamLink; + + switch (settings.store.timestampMode) { + case TimestampMode.NOW: + activity.timestamps = { + start: Math.floor(Date.now() / 1000) + }; + break; + case TimestampMode.TIME: + activity.timestamps = { + start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds() + }; + break; + case TimestampMode.CUSTOM: + if (startTime || endTime) { + activity.timestamps = {}; + if (startTime) activity.timestamps.start = startTime; + if (endTime) activity.timestamps.end = endTime; + } + break; + case TimestampMode.NONE: + default: + break; + } + + if (buttonOneText) { + activity.buttons = [ + buttonOneText, + buttonTwoText + ].filter(isTruthy); + + activity.metadata = { + button_urls: [ + buttonOneURL, + buttonTwoURL + ].filter(isTruthy) + }; + } + + if (imageBig) { + activity.assets = { + large_image: await getApplicationAsset(imageBig), + large_text: imageBigTooltip || undefined + }; + } + + if (imageSmall) { + activity.assets = { + ...activity.assets, + small_image: await getApplicationAsset(imageSmall), + small_text: imageSmallTooltip || undefined + }; + } + + + for (const k in activity) { + if (k === "type") continue; + const v = activity[k]; + if (!v || v.length === 0) + delete activity[k]; + } + + return activity; +} + +async function setRpc(disable?: boolean) { + const activity: Activity | undefined = await createActivity(); + + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity: !disable ? activity : null, + socketId: "CustomRPC", + }); +} + +export default definePlugin({ + name: "CustomRPC", + description: "Allows you to set a custom rich presence.", + authors: [Devs.captain, Devs.AutumnVN], + start: setRpc, + stop: () => setRpc(true), + settings, + + settingsAboutComponent: () => { + const activity = useAwaiter(createActivity); + return ( + <> + + Go to Discord Deverloper Portal to create an application and + get the application ID. + + + Upload images in the Rich Presence tab to get the image keys. + + + If you want to use image link, download your image and reupload the image to Imgur and get the image link by right-clicking the image and select "Copy image address". + + +
+ {activity[0] && } +
+ + ); + } +}); diff --git a/src/plugins/devCompanion.dev.tsx b/src/plugins/devCompanion.dev.tsx deleted file mode 100644 index 2fa01f2..0000000 --- a/src/plugins/devCompanion.dev.tsx +++ /dev/null @@ -1,260 +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 { showNotification } from "@api/Notifications"; -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; -import definePlugin, { OptionType } from "@utils/types"; -import { filters, findAll, search } from "@webpack"; - -const PORT = 8485; -const NAV_ID = "dev-companion-reconnect"; - -const logger = new Logger("DevCompanion"); - -let socket: WebSocket | undefined; - -type Node = StringNode | RegexNode | FunctionNode; - -interface StringNode { - type: "string"; - value: string; -} - -interface RegexNode { - type: "regex"; - value: { - pattern: string; - flags: string; - }; -} - -interface FunctionNode { - type: "function"; - value: string; -} - -interface PatchData { - find: string; - replacement: { - match: StringNode | RegexNode; - replace: StringNode | FunctionNode; - }[]; -} - -interface FindData { - type: string; - args: Array; -} - -const settings = definePluginSettings({ - notifyOnAutoConnect: { - description: "Whether to notify when Dev Companion has automatically connected.", - type: OptionType.BOOLEAN, - default: true - } -}); - -function parseNode(node: Node) { - switch (node.type) { - case "string": - return node.value; - case "regex": - return new RegExp(node.value.pattern, node.value.flags); - case "function": - // We LOVE remote code execution - // Safety: This comes from localhost only, which actually means we have less permissions than the source, - // since we're running in the browser sandbox, whereas the sender has host access - return (0, eval)(node.value); - default: - throw new Error("Unknown Node Type " + (node as any).type); - } -} - -function initWs(isManual = false) { - let wasConnected = isManual; - let hasErrored = false; - const ws = socket = new WebSocket(`ws://localhost:${PORT}`); - - ws.addEventListener("open", () => { - wasConnected = true; - - logger.info("Connected to WebSocket"); - - (settings.store.notifyOnAutoConnect || isManual) && showNotification({ - title: "Dev Companion Connected", - body: "Connected to WebSocket", - noPersist: true - }); - }); - - ws.addEventListener("error", e => { - if (!wasConnected) return; - - hasErrored = true; - - logger.error("Dev Companion Error:", e); - - showNotification({ - title: "Dev Companion Error", - body: (e as ErrorEvent).message || "No Error Message", - color: "var(--status-danger, red)", - noPersist: true, - }); - }); - - ws.addEventListener("close", e => { - if (!wasConnected || hasErrored) return; - - logger.info("Dev Companion Disconnected:", e.code, e.reason); - - showNotification({ - title: "Dev Companion Disconnected", - body: e.reason || "No Reason provided", - color: "var(--status-danger, red)", - noPersist: true, - }); - }); - - ws.addEventListener("message", e => { - try { - var { nonce, type, data } = JSON.parse(e.data); - } catch (err) { - logger.error("Invalid JSON:", err, "\n" + e.data); - return; - } - - function reply(error?: string) { - const data = { nonce, ok: !error } as Record; - if (error) data.error = error; - - ws.send(JSON.stringify(data)); - } - - logger.info("Received Message:", type, "\n", data); - - switch (type) { - case "testPatch": { - const { find, replacement } = data as PatchData; - - const candidates = search(find); - const keys = Object.keys(candidates); - if (keys.length !== 1) - return reply("Expected exactly one 'find' matches, found " + keys.length); - - const mod = candidates[keys[0]]; - let src = String(mod.original ?? mod).replaceAll("\n", ""); - - if (src.startsWith("function(")) { - src = "0," + src; - } - - let i = 0; - - for (const { match, replace } of replacement) { - i++; - - try { - const matcher = canonicalizeMatch(parseNode(match)); - const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName"); - - const newSource = src.replace(matcher, replacement as string); - - if (src === newSource) throw "Had no effect"; - Function(newSource); - - src = newSource; - } catch (err) { - return reply(`Replacement ${i} failed: ${err}`); - } - } - - reply(); - break; - } - case "testFind": { - const { type, args } = data as FindData; - try { - var parsedArgs = args.map(parseNode); - } catch (err) { - return reply("Failed to parse args: " + err); - } - - try { - let results: any[]; - switch (type.replace("find", "").replace("Lazy", "")) { - case "": - results = findAll(parsedArgs[0]); - break; - case "ByProps": - results = findAll(filters.byProps(...parsedArgs)); - break; - case "Store": - results = findAll(filters.byStoreName(parsedArgs[0])); - break; - case "ByCode": - results = findAll(filters.byCode(...parsedArgs)); - break; - case "ModuleId": - results = Object.keys(search(parsedArgs[0])); - break; - default: - return reply("Unknown Find Type " + type); - } - - const uniqueResultsCount = new Set(results).size; - if (uniqueResultsCount === 0) throw "No results"; - if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific"; - } catch (err) { - return reply("Failed to find: " + err); - } - - reply(); - break; - } - default: - reply("Unknown Type " + type); - break; - } - }); -} - -export default definePlugin({ - name: "DevCompanion", - description: "Dev Companion Plugin", - authors: [Devs.Ven], - settings, - - toolboxActions: { - "Reconnect"() { - socket?.close(1000, "Reconnecting"); - initWs(true); - } - }, - - start() { - initWs(); - }, - - stop() { - socket?.close(1000, "Plugin Stopped"); - socket = void 0; - } -}); diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx new file mode 100644 index 0000000..2fa01f2 --- /dev/null +++ b/src/plugins/devCompanion.dev/index.tsx @@ -0,0 +1,260 @@ +/* + * 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 { showNotification } from "@api/Notifications"; +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; +import definePlugin, { OptionType } from "@utils/types"; +import { filters, findAll, search } from "@webpack"; + +const PORT = 8485; +const NAV_ID = "dev-companion-reconnect"; + +const logger = new Logger("DevCompanion"); + +let socket: WebSocket | undefined; + +type Node = StringNode | RegexNode | FunctionNode; + +interface StringNode { + type: "string"; + value: string; +} + +interface RegexNode { + type: "regex"; + value: { + pattern: string; + flags: string; + }; +} + +interface FunctionNode { + type: "function"; + value: string; +} + +interface PatchData { + find: string; + replacement: { + match: StringNode | RegexNode; + replace: StringNode | FunctionNode; + }[]; +} + +interface FindData { + type: string; + args: Array; +} + +const settings = definePluginSettings({ + notifyOnAutoConnect: { + description: "Whether to notify when Dev Companion has automatically connected.", + type: OptionType.BOOLEAN, + default: true + } +}); + +function parseNode(node: Node) { + switch (node.type) { + case "string": + return node.value; + case "regex": + return new RegExp(node.value.pattern, node.value.flags); + case "function": + // We LOVE remote code execution + // Safety: This comes from localhost only, which actually means we have less permissions than the source, + // since we're running in the browser sandbox, whereas the sender has host access + return (0, eval)(node.value); + default: + throw new Error("Unknown Node Type " + (node as any).type); + } +} + +function initWs(isManual = false) { + let wasConnected = isManual; + let hasErrored = false; + const ws = socket = new WebSocket(`ws://localhost:${PORT}`); + + ws.addEventListener("open", () => { + wasConnected = true; + + logger.info("Connected to WebSocket"); + + (settings.store.notifyOnAutoConnect || isManual) && showNotification({ + title: "Dev Companion Connected", + body: "Connected to WebSocket", + noPersist: true + }); + }); + + ws.addEventListener("error", e => { + if (!wasConnected) return; + + hasErrored = true; + + logger.error("Dev Companion Error:", e); + + showNotification({ + title: "Dev Companion Error", + body: (e as ErrorEvent).message || "No Error Message", + color: "var(--status-danger, red)", + noPersist: true, + }); + }); + + ws.addEventListener("close", e => { + if (!wasConnected || hasErrored) return; + + logger.info("Dev Companion Disconnected:", e.code, e.reason); + + showNotification({ + title: "Dev Companion Disconnected", + body: e.reason || "No Reason provided", + color: "var(--status-danger, red)", + noPersist: true, + }); + }); + + ws.addEventListener("message", e => { + try { + var { nonce, type, data } = JSON.parse(e.data); + } catch (err) { + logger.error("Invalid JSON:", err, "\n" + e.data); + return; + } + + function reply(error?: string) { + const data = { nonce, ok: !error } as Record; + if (error) data.error = error; + + ws.send(JSON.stringify(data)); + } + + logger.info("Received Message:", type, "\n", data); + + switch (type) { + case "testPatch": { + const { find, replacement } = data as PatchData; + + const candidates = search(find); + const keys = Object.keys(candidates); + if (keys.length !== 1) + return reply("Expected exactly one 'find' matches, found " + keys.length); + + const mod = candidates[keys[0]]; + let src = String(mod.original ?? mod).replaceAll("\n", ""); + + if (src.startsWith("function(")) { + src = "0," + src; + } + + let i = 0; + + for (const { match, replace } of replacement) { + i++; + + try { + const matcher = canonicalizeMatch(parseNode(match)); + const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName"); + + const newSource = src.replace(matcher, replacement as string); + + if (src === newSource) throw "Had no effect"; + Function(newSource); + + src = newSource; + } catch (err) { + return reply(`Replacement ${i} failed: ${err}`); + } + } + + reply(); + break; + } + case "testFind": { + const { type, args } = data as FindData; + try { + var parsedArgs = args.map(parseNode); + } catch (err) { + return reply("Failed to parse args: " + err); + } + + try { + let results: any[]; + switch (type.replace("find", "").replace("Lazy", "")) { + case "": + results = findAll(parsedArgs[0]); + break; + case "ByProps": + results = findAll(filters.byProps(...parsedArgs)); + break; + case "Store": + results = findAll(filters.byStoreName(parsedArgs[0])); + break; + case "ByCode": + results = findAll(filters.byCode(...parsedArgs)); + break; + case "ModuleId": + results = Object.keys(search(parsedArgs[0])); + break; + default: + return reply("Unknown Find Type " + type); + } + + const uniqueResultsCount = new Set(results).size; + if (uniqueResultsCount === 0) throw "No results"; + if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific"; + } catch (err) { + return reply("Failed to find: " + err); + } + + reply(); + break; + } + default: + reply("Unknown Type " + type); + break; + } + }); +} + +export default definePlugin({ + name: "DevCompanion", + description: "Dev Companion Plugin", + authors: [Devs.Ven], + settings, + + toolboxActions: { + "Reconnect"() { + socket?.close(1000, "Reconnecting"); + initWs(true); + } + }, + + start() { + initWs(); + }, + + stop() { + socket?.close(1000, "Plugin Stopped"); + socket = void 0; + } +}); diff --git a/src/plugins/disableDMCallIdle.ts b/src/plugins/disableDMCallIdle.ts deleted file mode 100644 index 26ea3cd..0000000 --- a/src/plugins/disableDMCallIdle.ts +++ /dev/null @@ -1,35 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "DisableDMCallIdle", - description: "Disables automatically getting kicked from a DM voice call after 3 minutes.", - authors: [Devs.Nuckyz], - patches: [ - { - find: ".Messages.BOT_CALL_IDLE_DISCONNECT", - replacement: { - match: /(?<=function \i\(\){)(?=.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/, - replace: "return;" - } - } - ] -}); diff --git a/src/plugins/disableDMCallIdle/index.ts b/src/plugins/disableDMCallIdle/index.ts new file mode 100644 index 0000000..26ea3cd --- /dev/null +++ b/src/plugins/disableDMCallIdle/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "DisableDMCallIdle", + description: "Disables automatically getting kicked from a DM voice call after 3 minutes.", + authors: [Devs.Nuckyz], + patches: [ + { + find: ".Messages.BOT_CALL_IDLE_DISCONNECT", + replacement: { + match: /(?<=function \i\(\){)(?=.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/, + replace: "return;" + } + } + ] +}); diff --git a/src/plugins/emoteCloner.tsx b/src/plugins/emoteCloner.tsx deleted file mode 100644 index 0900422..0000000 --- a/src/plugins/emoteCloner.tsx +++ /dev/null @@ -1,373 +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 { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; -import { CheckedTextInput } from "@components/CheckedTextInput"; -import { Devs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import { Margins } from "@utils/margins"; -import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal"; -import definePlugin from "@utils/types"; -import { findByCodeLazy, findStoreLazy } from "@webpack"; -import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common"; -import { Promisable } from "type-fest"; - -const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n; - -const StickersStore = findStoreLazy("StickersStore"); -const uploadEmoji = findByCodeLazy('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS("); - -interface Sticker { - t: "Sticker"; - description: string; - format_type: number; - guild_id: string; - id: string; - name: string; - tags: string; - type: number; -} - -interface Emoji { - t: "Emoji"; - id: string; - name: string; - isAnimated: boolean; -} - -type Data = Emoji | Sticker; - -const StickerExt = [, "png", "png", "json", "gif"] as const; - -function getUrl(data: Data) { - if (data.t === "Emoji") - return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`; - - return `${location.origin}/stickers/${data.id}.${StickerExt[data.format_type]}`; -} - -async function fetchSticker(id: string) { - const cached = StickersStore.getStickerById(id); - if (cached) return cached; - - const { body } = await RestAPI.get({ - url: `/stickers/${id}` - }); - - FluxDispatcher.dispatch({ - type: "STICKER_FETCH_SUCCESS", - sticker: body - }); - - return body as Sticker; -} - -async function cloneSticker(guildId: string, sticker: Sticker) { - const data = new FormData(); - data.append("name", sticker.name); - data.append("tags", sticker.tags); - data.append("description", sticker.description); - data.append("file", await fetchBlob(getUrl(sticker))); - - const { body } = await RestAPI.post({ - url: `/guilds/${guildId}/stickers`, - body: data, - }); - - FluxDispatcher.dispatch({ - type: "GUILD_STICKERS_CREATE_SUCCESS", - guildId, - sticker: { - ...body, - user: UserStore.getCurrentUser() - } - }); -} - -async function cloneEmoji(guildId: string, emoji: Emoji) { - const data = await fetchBlob(getUrl(emoji)); - - const dataUrl = await new Promise(resolve => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.readAsDataURL(data); - }); - - return uploadEmoji({ - guildId, - name: emoji.name.split("~")[0], - image: dataUrl - }); -} - -function getGuildCandidates(data: Data) { - const meId = UserStore.getCurrentUser().id; - - return Object.values(GuildStore.getGuilds()).filter(g => { - const canCreate = g.ownerId === meId || - BigInt(PermissionStore.getGuildPermissions({ id: g.id }) & MANAGE_EMOJIS_AND_STICKERS) === MANAGE_EMOJIS_AND_STICKERS; - if (!canCreate) return false; - - if (data.t === "Sticker") return true; - - const { isAnimated } = data as Emoji; - - const emojiSlots = g.getMaxEmojiSlots(); - const { emojis } = EmojiStore.getGuilds()[g.id]; - - let count = 0; - for (const emoji of emojis) - if (emoji.animated === isAnimated) count++; - return count < emojiSlots; - }).sort((a, b) => a.name.localeCompare(b.name)); -} - -async function fetchBlob(url: string) { - const res = await fetch(url); - if (!res.ok) - throw new Error(`Failed to fetch ${url} - ${res.status}`); - - return res.blob(); -} - -async function doClone(guildId: string, data: Sticker | Emoji) { - try { - if (data.t === "Sticker") - await cloneSticker(guildId, data); - else - await cloneEmoji(guildId, data); - - Toasts.show({ - message: `Successfully cloned ${data.name} to ${GuildStore.getGuild(guildId)?.name ?? "your server"}!`, - type: Toasts.Type.SUCCESS, - id: Toasts.genId() - }); - } catch (e) { - new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e); - Toasts.show({ - message: "Oopsie something went wrong :( Check console!!!", - type: Toasts.Type.FAILURE, - id: Toasts.genId() - }); - } -} - -const getFontSize = (s: string) => { - // [18, 18, 16, 16, 14, 12, 10] - const sizes = [20, 20, 18, 18, 16, 14, 12]; - return sizes[s.length] ?? 4; -}; - -const nameValidator = /^\w+$/i; - -function CloneModal({ data }: { data: Sticker | Emoji; }) { - const [isCloning, setIsCloning] = React.useState(false); - const [name, setName] = React.useState(data.name); - - const [x, invalidateMemo] = React.useReducer(x => x + 1, 0); - - const guilds = React.useMemo(() => getGuildCandidates(data), [data.id, x]); - - return ( - <> - Custom Name - { - data.name = v; - setName(v); - }} - validate={v => - (data.t === "Emoji" && v.length > 2 && v.length < 32 && nameValidator.test(v)) - || (data.t === "Sticker" && v.length > 2 && v.length < 30) - || "Name must be between 2 and 32 characters and only contain alphanumeric characters" - } - /> -
- {guilds.map(g => ( - - {({ onMouseLeave, onMouseEnter }) => ( -
{ - setIsCloning(true); - doClone(g.id, data).finally(() => { - invalidateMemo(); - setIsCloning(false); - }); - }} - > - {g.icon ? ( - {g.name} - ) : ( - - {g.acronym} - - )} -
- )} -
- ))} -
- - ); -} - -function buildMenuItem(type: "Emoji" | "Sticker", fetchData: () => Promisable>) { - return ( - - openModalLazy(async () => { - const res = await fetchData(); - const data = { t: type, ...res } as Sticker | Emoji; - const url = getUrl(data); - - return modalProps => ( - - - - Clone {data.name} - - - - - - ); - }) - } - /> - ); -} - -function isGifUrl(url: string) { - return new URL(url).pathname.endsWith(".gif"); -} - -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {}; - - if (!favoriteableId) return; - - const menuItem = (() => { - switch (favoriteableType) { - case "emoji": - const match = props.message.content.match(RegExp(`|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`)); - if (!match) return; - const name = match[1] ?? "FakeNitroEmoji"; - - return buildMenuItem("Emoji", () => ({ - id: favoriteableId, - name, - isAnimated: isGifUrl(itemHref ?? itemSrc) - })); - case "sticker": - const sticker = props.message.stickerItems.find(s => s.id === favoriteableId); - if (sticker?.format_type === 3 /* LOTTIE */) return; - - return buildMenuItem("Sticker", () => fetchSticker(favoriteableId)); - } - })(); - - if (menuItem) - findGroupChildrenByChildId("copy-link", children)?.push(menuItem); -}; - -const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => { - const { id, name, type } = props?.target?.dataset ?? {}; - if (!id) return; - - if (type === "emoji" && name) { - const firstChild = props.target.firstChild as HTMLImageElement; - - children.push(buildMenuItem("Emoji", () => ({ - id, - name, - isAnimated: firstChild && isGifUrl(firstChild.src) - }))); - } else if (type === "sticker" && !props.target.className?.includes("lottieCanvas")) { - children.push(buildMenuItem("Sticker", () => fetchSticker(id))); - } -}; - -export default definePlugin({ - name: "EmoteCloner", - 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); - } -}); diff --git a/src/plugins/emoteCloner/index.tsx b/src/plugins/emoteCloner/index.tsx new file mode 100644 index 0000000..0900422 --- /dev/null +++ b/src/plugins/emoteCloner/index.tsx @@ -0,0 +1,373 @@ +/* + * 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 { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { CheckedTextInput } from "@components/CheckedTextInput"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import { Margins } from "@utils/margins"; +import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal"; +import definePlugin from "@utils/types"; +import { findByCodeLazy, findStoreLazy } from "@webpack"; +import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common"; +import { Promisable } from "type-fest"; + +const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n; + +const StickersStore = findStoreLazy("StickersStore"); +const uploadEmoji = findByCodeLazy('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS("); + +interface Sticker { + t: "Sticker"; + description: string; + format_type: number; + guild_id: string; + id: string; + name: string; + tags: string; + type: number; +} + +interface Emoji { + t: "Emoji"; + id: string; + name: string; + isAnimated: boolean; +} + +type Data = Emoji | Sticker; + +const StickerExt = [, "png", "png", "json", "gif"] as const; + +function getUrl(data: Data) { + if (data.t === "Emoji") + return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`; + + return `${location.origin}/stickers/${data.id}.${StickerExt[data.format_type]}`; +} + +async function fetchSticker(id: string) { + const cached = StickersStore.getStickerById(id); + if (cached) return cached; + + const { body } = await RestAPI.get({ + url: `/stickers/${id}` + }); + + FluxDispatcher.dispatch({ + type: "STICKER_FETCH_SUCCESS", + sticker: body + }); + + return body as Sticker; +} + +async function cloneSticker(guildId: string, sticker: Sticker) { + const data = new FormData(); + data.append("name", sticker.name); + data.append("tags", sticker.tags); + data.append("description", sticker.description); + data.append("file", await fetchBlob(getUrl(sticker))); + + const { body } = await RestAPI.post({ + url: `/guilds/${guildId}/stickers`, + body: data, + }); + + FluxDispatcher.dispatch({ + type: "GUILD_STICKERS_CREATE_SUCCESS", + guildId, + sticker: { + ...body, + user: UserStore.getCurrentUser() + } + }); +} + +async function cloneEmoji(guildId: string, emoji: Emoji) { + const data = await fetchBlob(getUrl(emoji)); + + const dataUrl = await new Promise(resolve => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsDataURL(data); + }); + + return uploadEmoji({ + guildId, + name: emoji.name.split("~")[0], + image: dataUrl + }); +} + +function getGuildCandidates(data: Data) { + const meId = UserStore.getCurrentUser().id; + + return Object.values(GuildStore.getGuilds()).filter(g => { + const canCreate = g.ownerId === meId || + BigInt(PermissionStore.getGuildPermissions({ id: g.id }) & MANAGE_EMOJIS_AND_STICKERS) === MANAGE_EMOJIS_AND_STICKERS; + if (!canCreate) return false; + + if (data.t === "Sticker") return true; + + const { isAnimated } = data as Emoji; + + const emojiSlots = g.getMaxEmojiSlots(); + const { emojis } = EmojiStore.getGuilds()[g.id]; + + let count = 0; + for (const emoji of emojis) + if (emoji.animated === isAnimated) count++; + return count < emojiSlots; + }).sort((a, b) => a.name.localeCompare(b.name)); +} + +async function fetchBlob(url: string) { + const res = await fetch(url); + if (!res.ok) + throw new Error(`Failed to fetch ${url} - ${res.status}`); + + return res.blob(); +} + +async function doClone(guildId: string, data: Sticker | Emoji) { + try { + if (data.t === "Sticker") + await cloneSticker(guildId, data); + else + await cloneEmoji(guildId, data); + + Toasts.show({ + message: `Successfully cloned ${data.name} to ${GuildStore.getGuild(guildId)?.name ?? "your server"}!`, + type: Toasts.Type.SUCCESS, + id: Toasts.genId() + }); + } catch (e) { + new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e); + Toasts.show({ + message: "Oopsie something went wrong :( Check console!!!", + type: Toasts.Type.FAILURE, + id: Toasts.genId() + }); + } +} + +const getFontSize = (s: string) => { + // [18, 18, 16, 16, 14, 12, 10] + const sizes = [20, 20, 18, 18, 16, 14, 12]; + return sizes[s.length] ?? 4; +}; + +const nameValidator = /^\w+$/i; + +function CloneModal({ data }: { data: Sticker | Emoji; }) { + const [isCloning, setIsCloning] = React.useState(false); + const [name, setName] = React.useState(data.name); + + const [x, invalidateMemo] = React.useReducer(x => x + 1, 0); + + const guilds = React.useMemo(() => getGuildCandidates(data), [data.id, x]); + + return ( + <> + Custom Name + { + data.name = v; + setName(v); + }} + validate={v => + (data.t === "Emoji" && v.length > 2 && v.length < 32 && nameValidator.test(v)) + || (data.t === "Sticker" && v.length > 2 && v.length < 30) + || "Name must be between 2 and 32 characters and only contain alphanumeric characters" + } + /> +
+ {guilds.map(g => ( + + {({ onMouseLeave, onMouseEnter }) => ( +
{ + setIsCloning(true); + doClone(g.id, data).finally(() => { + invalidateMemo(); + setIsCloning(false); + }); + }} + > + {g.icon ? ( + {g.name} + ) : ( + + {g.acronym} + + )} +
+ )} +
+ ))} +
+ + ); +} + +function buildMenuItem(type: "Emoji" | "Sticker", fetchData: () => Promisable>) { + return ( + + openModalLazy(async () => { + const res = await fetchData(); + const data = { t: type, ...res } as Sticker | Emoji; + const url = getUrl(data); + + return modalProps => ( + + + + Clone {data.name} + + + + + + ); + }) + } + /> + ); +} + +function isGifUrl(url: string) { + return new URL(url).pathname.endsWith(".gif"); +} + +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {}; + + if (!favoriteableId) return; + + const menuItem = (() => { + switch (favoriteableType) { + case "emoji": + const match = props.message.content.match(RegExp(`|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`)); + if (!match) return; + const name = match[1] ?? "FakeNitroEmoji"; + + return buildMenuItem("Emoji", () => ({ + id: favoriteableId, + name, + isAnimated: isGifUrl(itemHref ?? itemSrc) + })); + case "sticker": + const sticker = props.message.stickerItems.find(s => s.id === favoriteableId); + if (sticker?.format_type === 3 /* LOTTIE */) return; + + return buildMenuItem("Sticker", () => fetchSticker(favoriteableId)); + } + })(); + + if (menuItem) + findGroupChildrenByChildId("copy-link", children)?.push(menuItem); +}; + +const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => { + const { id, name, type } = props?.target?.dataset ?? {}; + if (!id) return; + + if (type === "emoji" && name) { + const firstChild = props.target.firstChild as HTMLImageElement; + + children.push(buildMenuItem("Emoji", () => ({ + id, + name, + isAnimated: firstChild && isGifUrl(firstChild.src) + }))); + } else if (type === "sticker" && !props.target.className?.includes("lottieCanvas")) { + children.push(buildMenuItem("Sticker", () => fetchSticker(id))); + } +}; + +export default definePlugin({ + name: "EmoteCloner", + 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); + } +}); diff --git a/src/plugins/experiments.tsx b/src/plugins/experiments.tsx deleted file mode 100644 index d38687f..0000000 --- a/src/plugins/experiments.tsx +++ /dev/null @@ -1,137 +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 { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { ErrorCard } from "@components/ErrorCard"; -import { Devs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { Forms, React } from "@webpack/common"; - -const KbdStyles = findByPropsLazy("key", "removeBuildOverride"); - -const settings = definePluginSettings({ - enableIsStaff: { - description: "Enable isStaff", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: true - }, - forceStagingBanner: { - description: "Whether to force Staging banner under user area.", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: true - } -}); - -export default definePlugin({ - name: "Experiments", - description: "Enable Access to Experiments in Discord!", - authors: [ - Devs.Megu, - Devs.Ven, - Devs.Nickyux, - Devs.BanTheNons, - Devs.Nuckyz - ], - settings, - - patches: [ - { - find: "Object.defineProperties(this,{isDeveloper", - replacement: { - match: /(?<={isDeveloper:\{[^}]+?,get:function\(\)\{return )\w/, - replace: "true" - } - }, - { - find: 'type:"user",revision', - replacement: { - match: /!(\i)&&"CONNECTION_OPEN".+?;/g, - replace: "$1=!0;" - } - }, - { - find: ".isStaff=function(){", - predicate: () => settings.store.enableIsStaff, - replacement: [ - { - match: /return\s*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/, - replace: (_, user, flags) => `return Vencord.Webpack.Common.UserStore.getCurrentUser()?.id===${user}.id||${user}.hasFlag(${flags}.STAFF)}` - }, - { - match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*?\|\|/, - replace: "hasFreePremium=function(){return ", - } - ] - }, - { - find: ".Messages.DEV_NOTICE_STAGING", - predicate: () => settings.store.forceStagingBanner, - replacement: { - match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/, - replace: "true" - } - }, - { - find: 'H1,title:"Experiments"', - replacement: { - match: 'title:"Experiments",children:[', - replace: "$&$self.WarningCard()," - } - } - ], - - settingsAboutComponent: () => { - const isMacOS = navigator.platform.includes("Mac"); - const modKey = isMacOS ? "cmd" : "ctrl"; - const altKey = isMacOS ? "opt" : "alt"; - return ( - - More Information - - You can enable client DevTools{" "} - {modKey} +{" "} - {altKey} +{" "} - O{" "} - after enabling isStaff below - - - and then toggling Enable DevTools in the Developer Options tab in settings. - - - ); - }, - - WarningCard: ErrorBoundary.wrap(() => ( - - Hold on!! - - - Experiments are unreleased Discord features. They might not work, or even break your client or get your account disabled. - - - - Only use experiments if you know what you're doing. Vencord is not responsible for any damage caused by enabling experiments. - - - ), { noop: true }) -}); diff --git a/src/plugins/experiments/index.tsx b/src/plugins/experiments/index.tsx new file mode 100644 index 0000000..d38687f --- /dev/null +++ b/src/plugins/experiments/index.tsx @@ -0,0 +1,137 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { ErrorCard } from "@components/ErrorCard"; +import { Devs } from "@utils/constants"; +import { Margins } from "@utils/margins"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Forms, React } from "@webpack/common"; + +const KbdStyles = findByPropsLazy("key", "removeBuildOverride"); + +const settings = definePluginSettings({ + enableIsStaff: { + description: "Enable isStaff", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + }, + forceStagingBanner: { + description: "Whether to force Staging banner under user area.", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + } +}); + +export default definePlugin({ + name: "Experiments", + description: "Enable Access to Experiments in Discord!", + authors: [ + Devs.Megu, + Devs.Ven, + Devs.Nickyux, + Devs.BanTheNons, + Devs.Nuckyz + ], + settings, + + patches: [ + { + find: "Object.defineProperties(this,{isDeveloper", + replacement: { + match: /(?<={isDeveloper:\{[^}]+?,get:function\(\)\{return )\w/, + replace: "true" + } + }, + { + find: 'type:"user",revision', + replacement: { + match: /!(\i)&&"CONNECTION_OPEN".+?;/g, + replace: "$1=!0;" + } + }, + { + find: ".isStaff=function(){", + predicate: () => settings.store.enableIsStaff, + replacement: [ + { + match: /return\s*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/, + replace: (_, user, flags) => `return Vencord.Webpack.Common.UserStore.getCurrentUser()?.id===${user}.id||${user}.hasFlag(${flags}.STAFF)}` + }, + { + match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*?\|\|/, + replace: "hasFreePremium=function(){return ", + } + ] + }, + { + find: ".Messages.DEV_NOTICE_STAGING", + predicate: () => settings.store.forceStagingBanner, + replacement: { + match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/, + replace: "true" + } + }, + { + find: 'H1,title:"Experiments"', + replacement: { + match: 'title:"Experiments",children:[', + replace: "$&$self.WarningCard()," + } + } + ], + + settingsAboutComponent: () => { + const isMacOS = navigator.platform.includes("Mac"); + const modKey = isMacOS ? "cmd" : "ctrl"; + const altKey = isMacOS ? "opt" : "alt"; + return ( + + More Information + + You can enable client DevTools{" "} + {modKey} +{" "} + {altKey} +{" "} + O{" "} + after enabling isStaff below + + + and then toggling Enable DevTools in the Developer Options tab in settings. + + + ); + }, + + WarningCard: ErrorBoundary.wrap(() => ( + + Hold on!! + + + Experiments are unreleased Discord features. They might not work, or even break your client or get your account disabled. + + + + Only use experiments if you know what you're doing. Vencord is not responsible for any damage caused by enabling experiments. + + + ), { noop: true }) +}); diff --git a/src/plugins/f8break.ts b/src/plugins/f8break.ts deleted file mode 100644 index 89dd2c9..0000000 --- a/src/plugins/f8break.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "F8Break", - description: "Pause the client when you press F8 with DevTools (+ breakpoints) open.", - authors: [Devs.lewisakura], - - start() { - window.addEventListener("keydown", this.event); - }, - - stop() { - window.removeEventListener("keydown", this.event); - }, - - event(e: KeyboardEvent) { - if (e.code === "F8") { - // Hi! You've just paused the client. Pressing F8 in DevTools or in the main window will unpause it again. - // It's up to you on what to do, friend. Happy travels! - debugger; - } - } -}); diff --git a/src/plugins/f8break/index.ts b/src/plugins/f8break/index.ts new file mode 100644 index 0000000..89dd2c9 --- /dev/null +++ b/src/plugins/f8break/index.ts @@ -0,0 +1,42 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "F8Break", + description: "Pause the client when you press F8 with DevTools (+ breakpoints) open.", + authors: [Devs.lewisakura], + + start() { + window.addEventListener("keydown", this.event); + }, + + stop() { + window.removeEventListener("keydown", this.event); + }, + + event(e: KeyboardEvent) { + if (e.code === "F8") { + // Hi! You've just paused the client. Pressing F8 in DevTools or in the main window will unpause it again. + // It's up to you on what to do, friend. Happy travels! + debugger; + } + } +}); diff --git a/src/plugins/fakeNitro.ts b/src/plugins/fakeNitro.ts deleted file mode 100644 index 07191e1..0000000 --- a/src/plugins/fakeNitro.ts +++ /dev/null @@ -1,811 +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 { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings, Settings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; -import { getCurrentGuild } from "@utils/discord"; -import { proxyLazy } from "@utils/lazy"; -import { Logger } from "@utils/Logger"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack"; -import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common"; -import type { Message } from "discord-types/general"; -import { applyPalette, GIFEncoder, quantize } from "gifenc"; -import type { ReactElement, ReactNode } from "react"; - -const DRAFT_TYPE = 0; -const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); -const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore"); -const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings"); -const ReaderFactory = findByPropsLazy("readerFactory"); -const StickerStore = findStoreLazy("StickersStore") as { - getPremiumPacks(): StickerPack[]; - getAllGuildStickers(): Map; - getStickerById(id: string): Sticker | undefined; -}; - -function searchProtoClass(localName: string, parentProtoClass: any) { - if (!parentProtoClass) return; - - const field = parentProtoClass.fields.find(field => field.localName === localName); - if (!field) return; - - const getter: any = Object.values(field).find(value => typeof value === "function"); - return getter?.(); -} - -const AppearanceSettingsProto = proxyLazy(() => searchProtoClass("appearance", PreloadedUserSettingsProtoHandler.ProtoClass)); -const ClientThemeSettingsProto = proxyLazy(() => searchProtoClass("clientThemeSettings", AppearanceSettingsProto)); - -const USE_EXTERNAL_EMOJIS = 1n << 18n; -const USE_EXTERNAL_STICKERS = 1n << 37n; - -const enum EmojiIntentions { - REACTION = 0, - STATUS = 1, - COMMUNITY_CONTENT = 2, - CHAT = 3, - GUILD_STICKER_RELATED_EMOJI = 4, - GUILD_ROLE_BENEFIT_EMOJI = 5, - COMMUNITY_CONTENT_ONLY = 6, - SOUNDBOARD = 7 -} - -const enum StickerType { - PNG = 1, - APNG = 2, - LOTTIE = 3, - // don't think you can even have gif stickers but the docs have it - GIF = 4 -} - -interface BaseSticker { - available: boolean; - description: string; - format_type: number; - id: string; - name: string; - tags: string; - type: number; -} -interface GuildSticker extends BaseSticker { - guild_id: string; -} -interface DiscordSticker extends BaseSticker { - pack_id: string; -} -type Sticker = GuildSticker | DiscordSticker; - -interface StickerPack { - id: string; - name: string; - sku_id: string; - description: string; - cover_sticker_id: string; - banner_asset_id: string; - stickers: Sticker[]; -} - -const enum FakeNoticeType { - Sticker, - Emoji -} - -const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/; -const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./; -const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/; - -const settings = definePluginSettings({ - enableEmojiBypass: { - description: "Allow sending fake emojis", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true - }, - emojiSize: { - description: "Size of the emojis when sending", - type: OptionType.SLIDER, - default: 48, - markers: [32, 48, 64, 128, 160, 256, 512] - }, - transformEmojis: { - description: "Whether to transform fake emojis into real ones", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true - }, - enableStickerBypass: { - description: "Allow sending fake stickers", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true - }, - stickerSize: { - description: "Size of the stickers when sending", - type: OptionType.SLIDER, - default: 160, - markers: [32, 64, 128, 160, 256, 512] - }, - transformStickers: { - description: "Whether to transform fake stickers into real ones", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true - }, - transformCompoundSentence: { - description: "Whether to transform fake stickers and emojis in compound sentences (sentences with more content than just the fake emoji or sticker link)", - type: OptionType.BOOLEAN, - default: false - }, - enableStreamQualityBypass: { - description: "Allow streaming in nitro quality", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true - } -}); - -export default definePlugin({ - name: "FakeNitro", - authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN], - description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.", - dependencies: ["MessageEventsAPI"], - - settings, - - patches: [ - { - find: ".PREMIUM_LOCKED;", - predicate: () => settings.store.enableEmojiBypass, - replacement: [ - { - match: /(?<=(\i)=\i\.intention)/, - replace: (_, intention) => `,fakeNitroIntention=${intention}` - }, - { - match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g, - replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0' - }, - { - match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, - replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))` - }, - { - match: /if\(!\i\.available/, - replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))` - } - ] - }, - { - find: "canUseAnimatedEmojis:function", - predicate: () => settings.store.enableEmojiBypass, - replacement: { - match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g, - replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` - } - }, - { - find: "canUseStickersEverywhere:function", - predicate: () => settings.store.enableStickerBypass, - replacement: { - match: /canUseStickersEverywhere:function\(\i\){/, - replace: "$&return true;" - }, - }, - { - find: "\"SENDABLE\"", - predicate: () => settings.store.enableStickerBypass, - replacement: { - match: /(\w+)\.available\?/, - replace: "true?" - } - }, - { - find: "canUseHighVideoUploadQuality:function", - predicate: () => settings.store.enableStreamQualityBypass, - replacement: [ - "canUseHighVideoUploadQuality", - // TODO: Remove the last two when they get removed from stable - "(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)", - ].map(func => { - return { - match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"), - replace: "$&return true;" - }; - }) - }, - { - find: "STREAM_FPS_OPTION.format", - predicate: () => settings.store.enableStreamQualityBypass, - replacement: { - match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g, - replace: "" - } - }, - { - find: "canUseClientThemes:function", - replacement: { - match: /canUseClientThemes:function\(\i\){/, - replace: "$&return true;" - } - }, - { - find: '.displayName="UserSettingsProtoStore"', - replacement: [ - { - match: /CONNECTION_OPEN:function\((\i)\){/, - replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);` - }, - { - match: /=(\i)\.local;/, - replace: (m, props) => `${m}${props}.local||$self.handleProtoChange(${props}.settings.proto);` - } - ] - }, - { - find: "updateTheme:function", - replacement: { - match: /(function \i\(\i\){var (\i)=\i\.backgroundGradientPresetId.+?)(\i\.\i\.updateAsync.+?theme=(.+?);.+?\),\i\))/, - replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});` - } - }, - { - find: '["strong","em","u","text","inlineCode","s","spoiler"]', - replacement: [ - { - predicate: () => settings.store.transformEmojis, - match: /1!==(\i)\.length\|\|1!==\i\.length/, - replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])` - }, - { - predicate: () => settings.store.transformEmojis || settings.store.transformStickers, - match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/, - replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);` - } - ] - }, - { - find: "renderEmbeds=function", - replacement: [ - { - predicate: () => settings.store.transformEmojis || settings.store.transformStickers, - match: /(renderEmbeds=function\((\i)\){)(.+?embeds\.map\(\(function\((\i)\){)/, - replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;` - }, - { - predicate: () => settings.store.transformStickers, - match: /renderStickersAccessories=function\((\i)\){var (\i)=\(0,\i\.\i\)\(\i\),/, - replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message}),` - }, - { - predicate: () => settings.store.transformStickers, - match: /renderAttachments=function\(\i\){var (\i)=\i.attachments.+?;/, - replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});` - } - ] - }, - { - find: ".STICKER_IN_MESSAGE_HOVER,", - predicate: () => settings.store.transformStickers, - replacement: [ - { - match: /var (\i)=\i\.renderableSticker,.{0,50}closePopout.+?channel:\i,closePopout:\i,/, - replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},` - }, - { - match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/, - replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!${props}.renderableSticker?.fake)` - } - ] - }, - { - find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,", - predicate: () => settings.store.transformEmojis, - replacement: { - match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<=(\i)=\i\.node.+?)/, - replace: (m, node) => `${m}fakeNitroNode:${node},` - } - }, - { - find: ".Messages.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION", - predicate: () => settings.store.transformEmojis, - replacement: { - match: /(?<=\.Messages\.EMOJI_POPOUT_ADDED_PACK_DESCRIPTION.+?return ).{0,1200}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?(?=}\()/, - replace: reactNode => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!arguments[0]?.fakeNitroNode?.fake)` - } - } - ], - - get guildId() { - return getCurrentGuild()?.id; - }, - - get canUseEmotes() { - return (UserStore.getCurrentUser().premiumType ?? 0) > 0; - }, - - get canUseStickers() { - return (UserStore.getCurrentUser().premiumType ?? 0) > 1; - }, - - handleProtoChange(proto: any, user: any) { - if (proto == null || typeof proto === "string" || !UserSettingsProtoStore || (!proto.appearance && !AppearanceSettingsProto)) return; - - const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0; - - if (premiumType !== 2) { - proto.appearance ??= AppearanceSettingsProto.create(); - - if (UserSettingsProtoStore.settings.appearance?.theme != null) { - proto.appearance.theme = UserSettingsProtoStore.settings.appearance.theme; - } - - if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null && ClientThemeSettingsProto) { - const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({ - backgroundGradientPresetId: { - value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value - } - }); - - proto.appearance.clientThemeSettings ??= clientThemeSettingsDummyProto; - proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId; - } - } - }, - - handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) { - const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0; - if (premiumType === 2 || backgroundGradientPresetId == null) return original(); - - if (!AppearanceSettingsProto || !ClientThemeSettingsProto || !ReaderFactory) return; - - const currentAppearanceProto = PreloadedUserSettingsProtoHandler.getCurrentValue().appearance; - - const newAppearanceProto = currentAppearanceProto != null - ? AppearanceSettingsProto.fromBinary(AppearanceSettingsProto.toBinary(currentAppearanceProto), ReaderFactory) - : AppearanceSettingsProto.create(); - - newAppearanceProto.theme = theme; - - const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({ - backgroundGradientPresetId: { - value: backgroundGradientPresetId - } - }); - - newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummyProto; - newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId; - - const proto = PreloadedUserSettingsProtoHandler.ProtoClass.create(); - proto.appearance = newAppearanceProto; - - FluxDispatcher.dispatch({ - type: "USER_SETTINGS_PROTO_UPDATE", - local: true, - partial: true, - settings: { - type: 1, - proto - } - }); - }, - - trimContent(content: Array) { - const firstContent = content[0]; - if (typeof firstContent === "string") content[0] = firstContent.trimStart(); - if (content[0] === "") content.shift(); - - const lastIndex = content.length - 1; - const lastContent = content[lastIndex]; - if (typeof lastContent === "string") content[lastIndex] = lastContent.trimEnd(); - if (content[lastIndex] === "") content.pop(); - }, - - clearEmptyArrayItems(array: Array) { - return array.filter(item => item != null); - }, - - ensureChildrenIsArray(child: ReactElement) { - if (!Array.isArray(child.props.children)) child.props.children = [child.props.children]; - }, - - patchFakeNitroEmojisOrRemoveStickersLinks(content: Array, inline: boolean) { - // If content has more than one child or it's a single ReactElement like a header or list - if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content; - - let nextIndex = content.length; - - const transformLinkChild = (child: ReactElement) => { - if (settings.store.transformEmojis) { - const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex); - if (fakeNitroMatch) { - let url: URL | null = null; - try { - url = new URL(child.props.href); - } catch { } - - const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji"; - - return Parser.defaultRules.customEmoji.react({ - jumboable: !inline && content.length === 1 && typeof content[0].type !== "string", - animated: fakeNitroMatch[2] === "gif", - emojiId: fakeNitroMatch[1], - name: emojiName, - fake: true - }, void 0, { key: String(nextIndex++) }); - } - } - - if (settings.store.transformStickers) { - if (fakeNitroStickerRegex.test(child.props.href)) return null; - - const gifMatch = child.props.href.match(fakeNitroGifStickerRegex); - if (gifMatch) { - // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker - if (StickerStore.getStickerById(gifMatch[1])) return null; - } - } - - return child; - }; - - const transformChild = (child: ReactElement) => { - if (child?.props?.trusted != null) return transformLinkChild(child); - if (child?.props?.children != null) { - if (!Array.isArray(child.props.children)) { - child.props.children = modifyChild(child.props.children); - return child; - } - - child.props.children = modifyChildren(child.props.children); - if (child.props.children.length === 0) return null; - return child; - } - - return child; - }; - - const modifyChild = (child: ReactElement) => { - const newChild = transformChild(child); - - if (newChild?.type === "ul" || newChild?.type === "ol") { - this.ensureChildrenIsArray(newChild); - if (newChild.props.children.length === 0) return null; - - let listHasAnItem = false; - for (const [index, child] of newChild.props.children.entries()) { - if (child == null) { - delete newChild.props.children[index]; - continue; - } - - this.ensureChildrenIsArray(child); - if (child.props.children.length > 0) listHasAnItem = true; - else delete newChild.props.children[index]; - } - - if (!listHasAnItem) return null; - - newChild.props.children = this.clearEmptyArrayItems(newChild.props.children); - } - - return newChild; - }; - - const modifyChildren = (children: Array) => { - for (const [index, child] of children.entries()) children[index] = modifyChild(child); - - children = this.clearEmptyArrayItems(children); - this.trimContent(children); - - return children; - }; - - try { - return modifyChildren(window._.cloneDeep(content)); - } catch (err) { - new Logger("FakeNitro").error(err); - return content; - } - }, - - patchFakeNitroStickers(stickers: Array, message: Message) { - const itemsToMaybePush: Array = []; - - const contentItems = message.content.split(/\s/); - if (settings.store.transformCompoundSentence) itemsToMaybePush.push(...contentItems); - else if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]); - - itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url)); - - for (const item of itemsToMaybePush) { - if (!settings.store.transformCompoundSentence && !item.startsWith("http")) continue; - - const imgMatch = item.match(fakeNitroStickerRegex); - if (imgMatch) { - let url: URL | null = null; - try { - url = new URL(item); - } catch { } - - const stickerName = StickerStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroSticker"; - stickers.push({ - format_type: 1, - id: imgMatch[1], - name: stickerName, - fake: true - }); - - continue; - } - - const gifMatch = item.match(fakeNitroGifStickerRegex); - if (gifMatch) { - if (!StickerStore.getStickerById(gifMatch[1])) continue; - - const stickerName = StickerStore.getStickerById(gifMatch[1])?.name ?? "FakeNitroSticker"; - stickers.push({ - format_type: 2, - id: gifMatch[1], - name: stickerName, - fake: true - }); - } - } - - return stickers; - }, - - shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) { - const contentItems = message.content.split(/\s/); - if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false; - - switch (embed.type) { - case "image": { - if ( - !settings.store.transformCompoundSentence - && !contentItems.includes(embed.url!) - && !contentItems.includes(embed.image?.proxyURL!) - ) return false; - - if (settings.store.transformEmojis) { - if (fakeNitroEmojiRegex.test(embed.url!)) return true; - } - - if (settings.store.transformStickers) { - if (fakeNitroStickerRegex.test(embed.url!)) return true; - - const gifMatch = embed.url!.match(fakeNitroGifStickerRegex); - if (gifMatch) { - // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker - if (StickerStore.getStickerById(gifMatch[1])) return true; - } - } - - break; - } - } - - return false; - }, - - filterAttachments(attachments: Message["attachments"]) { - return attachments.filter(attachment => { - if (attachment.content_type !== "image/gif") return true; - - const match = attachment.url.match(fakeNitroGifStickerRegex); - if (match) { - // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker - if (StickerStore.getStickerById(match[1])) return false; - } - - return true; - }); - }, - - shouldKeepEmojiLink(link: any) { - return link.target && fakeNitroEmojiRegex.test(link.target); - }, - - addFakeNotice(type: FakeNoticeType, node: Array, fake: boolean) { - if (!fake) return node; - - node = Array.isArray(node) ? node : [node]; - - switch (type) { - case FakeNoticeType.Sticker: { - node.push(" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users."); - - return node; - } - case FakeNoticeType.Emoji: { - node.push(" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users."); - - return node; - } - } - }, - - 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.plugins.FakeNitro.stickerSize}`; - }, - - async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) { - const { parseURL } = importApngJs(); - - const { frames, width, height } = await parseURL(stickerLink); - - const gif = GIFEncoder(); - const resolution = Settings.plugins.FakeNitro.stickerSize; - - const canvas = document.createElement("canvas"); - canvas.width = resolution; - canvas.height = resolution; - - const ctx = canvas.getContext("2d", { - willReadFrequently: true - })!; - - const scale = resolution / Math.max(width, height); - ctx.scale(scale, scale); - - let previousFrameData: ImageData; - - for (const frame of frames) { - const { left, top, width, height, img, delay, blendOp, disposeOp } = frame; - - previousFrameData = ctx.getImageData(left, top, width, height); - - if (blendOp === ApngBlendOp.SOURCE) { - ctx.clearRect(left, top, width, height); - } - - ctx.drawImage(img, left, top, width, height); - - const { data } = ctx.getImageData(0, 0, resolution, resolution); - - const palette = quantize(data, 256); - const index = applyPalette(data, palette); - - gif.writeFrame(index, resolution, resolution, { - transparent: true, - palette, - delay - }); - - if (disposeOp === ApngDisposeOp.BACKGROUND) { - ctx.clearRect(left, top, width, height); - } else if (disposeOp === ApngDisposeOp.PREVIOUS) { - ctx.putImageData(previousFrameData, left, top); - } - } - - gif.finish(); - - const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" }); - promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE); - }, - - start() { - const s = settings.store; - - if (!s.enableEmojiBypass && !s.enableStickerBypass) { - return; - } - - function getWordBoundary(origStr: string, offset: number) { - return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " "; - } - - this.preSend = addPreSendListener((channelId, messageObj, extra) => { - const { guildId } = this; - - stickerBypass: { - if (!s.enableStickerBypass) - break stickerBypass; - - const sticker = StickerStore.getStickerById(extra.stickers?.[0]!); - if (!sticker) - break stickerBypass; - - // Discord Stickers are now free yayyy!! :D - if ("pack_id" in sticker) - break stickerBypass; - - const canUseStickers = this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId); - if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId)) - break stickerBypass; - - const link = this.getStickerLink(sticker.id); - if (sticker.format_type === StickerType.APNG) { - this.sendAnimatedSticker(link, sticker.id, channelId); - return { cancel: true }; - } else { - extra.stickers!.length = 0; - messageObj.content += ` ${link}&name=${encodeURIComponent(sticker.name)}`; - } - } - - if (s.enableEmojiBypass) { - const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(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; - - const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; - const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({ - size: Settings.plugins.FakeNitro.emojiSize, - name: encodeURIComponent(emoji.name) - })); - messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; - }); - } - } - - return { cancel: false }; - }); - - this.preEdit = addPreEditListener((channelId, __, messageObj) => { - if (!s.enableEmojiBypass) return; - - const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId); - - const { guildId } = this; - - messageObj.content = messageObj.content.replace(/(?/ig, (emojiStr, emojiId, offset, origStr) => { - const emoji = EmojiStore.getCustomEmojiById(emojiId); - if (emoji == null) return emojiStr; - if (!emoji.require_colons) return emojiStr; - if (emoji.available !== false && canUseEmotes) return emojiStr; - if (emoji.guildId === guildId && !emoji.animated) return emojiStr; - - const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({ - size: Settings.plugins.FakeNitro.emojiSize, - name: encodeURIComponent(emoji.name) - })); - return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + emojiStr.length)}`; - }); - }); - }, - - stop() { - removePreSendListener(this.preSend); - removePreEditListener(this.preEdit); - } -}); diff --git a/src/plugins/fakeNitro/index.ts b/src/plugins/fakeNitro/index.ts new file mode 100644 index 0000000..07191e1 --- /dev/null +++ b/src/plugins/fakeNitro/index.ts @@ -0,0 +1,811 @@ +/* + * 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 { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; +import { definePluginSettings, Settings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; +import { getCurrentGuild } from "@utils/discord"; +import { proxyLazy } from "@utils/lazy"; +import { Logger } from "@utils/Logger"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack"; +import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common"; +import type { Message } from "discord-types/general"; +import { applyPalette, GIFEncoder, quantize } from "gifenc"; +import type { ReactElement, ReactNode } from "react"; + +const DRAFT_TYPE = 0; +const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); +const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore"); +const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings"); +const ReaderFactory = findByPropsLazy("readerFactory"); +const StickerStore = findStoreLazy("StickersStore") as { + getPremiumPacks(): StickerPack[]; + getAllGuildStickers(): Map; + getStickerById(id: string): Sticker | undefined; +}; + +function searchProtoClass(localName: string, parentProtoClass: any) { + if (!parentProtoClass) return; + + const field = parentProtoClass.fields.find(field => field.localName === localName); + if (!field) return; + + const getter: any = Object.values(field).find(value => typeof value === "function"); + return getter?.(); +} + +const AppearanceSettingsProto = proxyLazy(() => searchProtoClass("appearance", PreloadedUserSettingsProtoHandler.ProtoClass)); +const ClientThemeSettingsProto = proxyLazy(() => searchProtoClass("clientThemeSettings", AppearanceSettingsProto)); + +const USE_EXTERNAL_EMOJIS = 1n << 18n; +const USE_EXTERNAL_STICKERS = 1n << 37n; + +const enum EmojiIntentions { + REACTION = 0, + STATUS = 1, + COMMUNITY_CONTENT = 2, + CHAT = 3, + GUILD_STICKER_RELATED_EMOJI = 4, + GUILD_ROLE_BENEFIT_EMOJI = 5, + COMMUNITY_CONTENT_ONLY = 6, + SOUNDBOARD = 7 +} + +const enum StickerType { + PNG = 1, + APNG = 2, + LOTTIE = 3, + // don't think you can even have gif stickers but the docs have it + GIF = 4 +} + +interface BaseSticker { + available: boolean; + description: string; + format_type: number; + id: string; + name: string; + tags: string; + type: number; +} +interface GuildSticker extends BaseSticker { + guild_id: string; +} +interface DiscordSticker extends BaseSticker { + pack_id: string; +} +type Sticker = GuildSticker | DiscordSticker; + +interface StickerPack { + id: string; + name: string; + sku_id: string; + description: string; + cover_sticker_id: string; + banner_asset_id: string; + stickers: Sticker[]; +} + +const enum FakeNoticeType { + Sticker, + Emoji +} + +const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/; +const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./; +const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/; + +const settings = definePluginSettings({ + enableEmojiBypass: { + description: "Allow sending fake emojis", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + }, + emojiSize: { + description: "Size of the emojis when sending", + type: OptionType.SLIDER, + default: 48, + markers: [32, 48, 64, 128, 160, 256, 512] + }, + transformEmojis: { + description: "Whether to transform fake emojis into real ones", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + }, + enableStickerBypass: { + description: "Allow sending fake stickers", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + }, + stickerSize: { + description: "Size of the stickers when sending", + type: OptionType.SLIDER, + default: 160, + markers: [32, 64, 128, 160, 256, 512] + }, + transformStickers: { + description: "Whether to transform fake stickers into real ones", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + }, + transformCompoundSentence: { + description: "Whether to transform fake stickers and emojis in compound sentences (sentences with more content than just the fake emoji or sticker link)", + type: OptionType.BOOLEAN, + default: false + }, + enableStreamQualityBypass: { + description: "Allow streaming in nitro quality", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + } +}); + +export default definePlugin({ + name: "FakeNitro", + authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN], + description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.", + dependencies: ["MessageEventsAPI"], + + settings, + + patches: [ + { + find: ".PREMIUM_LOCKED;", + predicate: () => settings.store.enableEmojiBypass, + replacement: [ + { + match: /(?<=(\i)=\i\.intention)/, + replace: (_, intention) => `,fakeNitroIntention=${intention}` + }, + { + match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g, + replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0' + }, + { + match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, + replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))` + }, + { + match: /if\(!\i\.available/, + replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))` + } + ] + }, + { + find: "canUseAnimatedEmojis:function", + predicate: () => settings.store.enableEmojiBypass, + replacement: { + match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g, + replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` + } + }, + { + find: "canUseStickersEverywhere:function", + predicate: () => settings.store.enableStickerBypass, + replacement: { + match: /canUseStickersEverywhere:function\(\i\){/, + replace: "$&return true;" + }, + }, + { + find: "\"SENDABLE\"", + predicate: () => settings.store.enableStickerBypass, + replacement: { + match: /(\w+)\.available\?/, + replace: "true?" + } + }, + { + find: "canUseHighVideoUploadQuality:function", + predicate: () => settings.store.enableStreamQualityBypass, + replacement: [ + "canUseHighVideoUploadQuality", + // TODO: Remove the last two when they get removed from stable + "(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)", + ].map(func => { + return { + match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"), + replace: "$&return true;" + }; + }) + }, + { + find: "STREAM_FPS_OPTION.format", + predicate: () => settings.store.enableStreamQualityBypass, + replacement: { + match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g, + replace: "" + } + }, + { + find: "canUseClientThemes:function", + replacement: { + match: /canUseClientThemes:function\(\i\){/, + replace: "$&return true;" + } + }, + { + find: '.displayName="UserSettingsProtoStore"', + replacement: [ + { + match: /CONNECTION_OPEN:function\((\i)\){/, + replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);` + }, + { + match: /=(\i)\.local;/, + replace: (m, props) => `${m}${props}.local||$self.handleProtoChange(${props}.settings.proto);` + } + ] + }, + { + find: "updateTheme:function", + replacement: { + match: /(function \i\(\i\){var (\i)=\i\.backgroundGradientPresetId.+?)(\i\.\i\.updateAsync.+?theme=(.+?);.+?\),\i\))/, + replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});` + } + }, + { + find: '["strong","em","u","text","inlineCode","s","spoiler"]', + replacement: [ + { + predicate: () => settings.store.transformEmojis, + match: /1!==(\i)\.length\|\|1!==\i\.length/, + replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])` + }, + { + predicate: () => settings.store.transformEmojis || settings.store.transformStickers, + match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/, + replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);` + } + ] + }, + { + find: "renderEmbeds=function", + replacement: [ + { + predicate: () => settings.store.transformEmojis || settings.store.transformStickers, + match: /(renderEmbeds=function\((\i)\){)(.+?embeds\.map\(\(function\((\i)\){)/, + replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;` + }, + { + predicate: () => settings.store.transformStickers, + match: /renderStickersAccessories=function\((\i)\){var (\i)=\(0,\i\.\i\)\(\i\),/, + replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message}),` + }, + { + predicate: () => settings.store.transformStickers, + match: /renderAttachments=function\(\i\){var (\i)=\i.attachments.+?;/, + replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});` + } + ] + }, + { + find: ".STICKER_IN_MESSAGE_HOVER,", + predicate: () => settings.store.transformStickers, + replacement: [ + { + match: /var (\i)=\i\.renderableSticker,.{0,50}closePopout.+?channel:\i,closePopout:\i,/, + replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},` + }, + { + match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/, + replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!${props}.renderableSticker?.fake)` + } + ] + }, + { + find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,", + predicate: () => settings.store.transformEmojis, + replacement: { + match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<=(\i)=\i\.node.+?)/, + replace: (m, node) => `${m}fakeNitroNode:${node},` + } + }, + { + find: ".Messages.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION", + predicate: () => settings.store.transformEmojis, + replacement: { + match: /(?<=\.Messages\.EMOJI_POPOUT_ADDED_PACK_DESCRIPTION.+?return ).{0,1200}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?(?=}\()/, + replace: reactNode => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!arguments[0]?.fakeNitroNode?.fake)` + } + } + ], + + get guildId() { + return getCurrentGuild()?.id; + }, + + get canUseEmotes() { + return (UserStore.getCurrentUser().premiumType ?? 0) > 0; + }, + + get canUseStickers() { + return (UserStore.getCurrentUser().premiumType ?? 0) > 1; + }, + + handleProtoChange(proto: any, user: any) { + if (proto == null || typeof proto === "string" || !UserSettingsProtoStore || (!proto.appearance && !AppearanceSettingsProto)) return; + + const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0; + + if (premiumType !== 2) { + proto.appearance ??= AppearanceSettingsProto.create(); + + if (UserSettingsProtoStore.settings.appearance?.theme != null) { + proto.appearance.theme = UserSettingsProtoStore.settings.appearance.theme; + } + + if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null && ClientThemeSettingsProto) { + const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({ + backgroundGradientPresetId: { + value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value + } + }); + + proto.appearance.clientThemeSettings ??= clientThemeSettingsDummyProto; + proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId; + } + } + }, + + handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) { + const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0; + if (premiumType === 2 || backgroundGradientPresetId == null) return original(); + + if (!AppearanceSettingsProto || !ClientThemeSettingsProto || !ReaderFactory) return; + + const currentAppearanceProto = PreloadedUserSettingsProtoHandler.getCurrentValue().appearance; + + const newAppearanceProto = currentAppearanceProto != null + ? AppearanceSettingsProto.fromBinary(AppearanceSettingsProto.toBinary(currentAppearanceProto), ReaderFactory) + : AppearanceSettingsProto.create(); + + newAppearanceProto.theme = theme; + + const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({ + backgroundGradientPresetId: { + value: backgroundGradientPresetId + } + }); + + newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummyProto; + newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId; + + const proto = PreloadedUserSettingsProtoHandler.ProtoClass.create(); + proto.appearance = newAppearanceProto; + + FluxDispatcher.dispatch({ + type: "USER_SETTINGS_PROTO_UPDATE", + local: true, + partial: true, + settings: { + type: 1, + proto + } + }); + }, + + trimContent(content: Array) { + const firstContent = content[0]; + if (typeof firstContent === "string") content[0] = firstContent.trimStart(); + if (content[0] === "") content.shift(); + + const lastIndex = content.length - 1; + const lastContent = content[lastIndex]; + if (typeof lastContent === "string") content[lastIndex] = lastContent.trimEnd(); + if (content[lastIndex] === "") content.pop(); + }, + + clearEmptyArrayItems(array: Array) { + return array.filter(item => item != null); + }, + + ensureChildrenIsArray(child: ReactElement) { + if (!Array.isArray(child.props.children)) child.props.children = [child.props.children]; + }, + + patchFakeNitroEmojisOrRemoveStickersLinks(content: Array, inline: boolean) { + // If content has more than one child or it's a single ReactElement like a header or list + if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content; + + let nextIndex = content.length; + + const transformLinkChild = (child: ReactElement) => { + if (settings.store.transformEmojis) { + const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex); + if (fakeNitroMatch) { + let url: URL | null = null; + try { + url = new URL(child.props.href); + } catch { } + + const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji"; + + return Parser.defaultRules.customEmoji.react({ + jumboable: !inline && content.length === 1 && typeof content[0].type !== "string", + animated: fakeNitroMatch[2] === "gif", + emojiId: fakeNitroMatch[1], + name: emojiName, + fake: true + }, void 0, { key: String(nextIndex++) }); + } + } + + if (settings.store.transformStickers) { + if (fakeNitroStickerRegex.test(child.props.href)) return null; + + const gifMatch = child.props.href.match(fakeNitroGifStickerRegex); + if (gifMatch) { + // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker + if (StickerStore.getStickerById(gifMatch[1])) return null; + } + } + + return child; + }; + + const transformChild = (child: ReactElement) => { + if (child?.props?.trusted != null) return transformLinkChild(child); + if (child?.props?.children != null) { + if (!Array.isArray(child.props.children)) { + child.props.children = modifyChild(child.props.children); + return child; + } + + child.props.children = modifyChildren(child.props.children); + if (child.props.children.length === 0) return null; + return child; + } + + return child; + }; + + const modifyChild = (child: ReactElement) => { + const newChild = transformChild(child); + + if (newChild?.type === "ul" || newChild?.type === "ol") { + this.ensureChildrenIsArray(newChild); + if (newChild.props.children.length === 0) return null; + + let listHasAnItem = false; + for (const [index, child] of newChild.props.children.entries()) { + if (child == null) { + delete newChild.props.children[index]; + continue; + } + + this.ensureChildrenIsArray(child); + if (child.props.children.length > 0) listHasAnItem = true; + else delete newChild.props.children[index]; + } + + if (!listHasAnItem) return null; + + newChild.props.children = this.clearEmptyArrayItems(newChild.props.children); + } + + return newChild; + }; + + const modifyChildren = (children: Array) => { + for (const [index, child] of children.entries()) children[index] = modifyChild(child); + + children = this.clearEmptyArrayItems(children); + this.trimContent(children); + + return children; + }; + + try { + return modifyChildren(window._.cloneDeep(content)); + } catch (err) { + new Logger("FakeNitro").error(err); + return content; + } + }, + + patchFakeNitroStickers(stickers: Array, message: Message) { + const itemsToMaybePush: Array = []; + + const contentItems = message.content.split(/\s/); + if (settings.store.transformCompoundSentence) itemsToMaybePush.push(...contentItems); + else if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]); + + itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url)); + + for (const item of itemsToMaybePush) { + if (!settings.store.transformCompoundSentence && !item.startsWith("http")) continue; + + const imgMatch = item.match(fakeNitroStickerRegex); + if (imgMatch) { + let url: URL | null = null; + try { + url = new URL(item); + } catch { } + + const stickerName = StickerStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroSticker"; + stickers.push({ + format_type: 1, + id: imgMatch[1], + name: stickerName, + fake: true + }); + + continue; + } + + const gifMatch = item.match(fakeNitroGifStickerRegex); + if (gifMatch) { + if (!StickerStore.getStickerById(gifMatch[1])) continue; + + const stickerName = StickerStore.getStickerById(gifMatch[1])?.name ?? "FakeNitroSticker"; + stickers.push({ + format_type: 2, + id: gifMatch[1], + name: stickerName, + fake: true + }); + } + } + + return stickers; + }, + + shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) { + const contentItems = message.content.split(/\s/); + if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false; + + switch (embed.type) { + case "image": { + if ( + !settings.store.transformCompoundSentence + && !contentItems.includes(embed.url!) + && !contentItems.includes(embed.image?.proxyURL!) + ) return false; + + if (settings.store.transformEmojis) { + if (fakeNitroEmojiRegex.test(embed.url!)) return true; + } + + if (settings.store.transformStickers) { + if (fakeNitroStickerRegex.test(embed.url!)) return true; + + const gifMatch = embed.url!.match(fakeNitroGifStickerRegex); + if (gifMatch) { + // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker + if (StickerStore.getStickerById(gifMatch[1])) return true; + } + } + + break; + } + } + + return false; + }, + + filterAttachments(attachments: Message["attachments"]) { + return attachments.filter(attachment => { + if (attachment.content_type !== "image/gif") return true; + + const match = attachment.url.match(fakeNitroGifStickerRegex); + if (match) { + // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker + if (StickerStore.getStickerById(match[1])) return false; + } + + return true; + }); + }, + + shouldKeepEmojiLink(link: any) { + return link.target && fakeNitroEmojiRegex.test(link.target); + }, + + addFakeNotice(type: FakeNoticeType, node: Array, fake: boolean) { + if (!fake) return node; + + node = Array.isArray(node) ? node : [node]; + + switch (type) { + case FakeNoticeType.Sticker: { + node.push(" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users."); + + return node; + } + case FakeNoticeType.Emoji: { + node.push(" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users."); + + return node; + } + } + }, + + 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.plugins.FakeNitro.stickerSize}`; + }, + + async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) { + const { parseURL } = importApngJs(); + + const { frames, width, height } = await parseURL(stickerLink); + + const gif = GIFEncoder(); + const resolution = Settings.plugins.FakeNitro.stickerSize; + + const canvas = document.createElement("canvas"); + canvas.width = resolution; + canvas.height = resolution; + + const ctx = canvas.getContext("2d", { + willReadFrequently: true + })!; + + const scale = resolution / Math.max(width, height); + ctx.scale(scale, scale); + + let previousFrameData: ImageData; + + for (const frame of frames) { + const { left, top, width, height, img, delay, blendOp, disposeOp } = frame; + + previousFrameData = ctx.getImageData(left, top, width, height); + + if (blendOp === ApngBlendOp.SOURCE) { + ctx.clearRect(left, top, width, height); + } + + ctx.drawImage(img, left, top, width, height); + + const { data } = ctx.getImageData(0, 0, resolution, resolution); + + const palette = quantize(data, 256); + const index = applyPalette(data, palette); + + gif.writeFrame(index, resolution, resolution, { + transparent: true, + palette, + delay + }); + + if (disposeOp === ApngDisposeOp.BACKGROUND) { + ctx.clearRect(left, top, width, height); + } else if (disposeOp === ApngDisposeOp.PREVIOUS) { + ctx.putImageData(previousFrameData, left, top); + } + } + + gif.finish(); + + const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" }); + promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE); + }, + + start() { + const s = settings.store; + + if (!s.enableEmojiBypass && !s.enableStickerBypass) { + return; + } + + function getWordBoundary(origStr: string, offset: number) { + return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " "; + } + + this.preSend = addPreSendListener((channelId, messageObj, extra) => { + const { guildId } = this; + + stickerBypass: { + if (!s.enableStickerBypass) + break stickerBypass; + + const sticker = StickerStore.getStickerById(extra.stickers?.[0]!); + if (!sticker) + break stickerBypass; + + // Discord Stickers are now free yayyy!! :D + if ("pack_id" in sticker) + break stickerBypass; + + const canUseStickers = this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId); + if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId)) + break stickerBypass; + + const link = this.getStickerLink(sticker.id); + if (sticker.format_type === StickerType.APNG) { + this.sendAnimatedSticker(link, sticker.id, channelId); + return { cancel: true }; + } else { + extra.stickers!.length = 0; + messageObj.content += ` ${link}&name=${encodeURIComponent(sticker.name)}`; + } + } + + if (s.enableEmojiBypass) { + const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(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; + + const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; + const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({ + size: Settings.plugins.FakeNitro.emojiSize, + name: encodeURIComponent(emoji.name) + })); + messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { + return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; + }); + } + } + + return { cancel: false }; + }); + + this.preEdit = addPreEditListener((channelId, __, messageObj) => { + if (!s.enableEmojiBypass) return; + + const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId); + + const { guildId } = this; + + messageObj.content = messageObj.content.replace(/(?/ig, (emojiStr, emojiId, offset, origStr) => { + const emoji = EmojiStore.getCustomEmojiById(emojiId); + if (emoji == null) return emojiStr; + if (!emoji.require_colons) return emojiStr; + if (emoji.available !== false && canUseEmotes) return emojiStr; + if (emoji.guildId === guildId && !emoji.animated) return emojiStr; + + const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({ + size: Settings.plugins.FakeNitro.emojiSize, + name: encodeURIComponent(emoji.name) + })); + return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + emojiStr.length)}`; + }); + }); + }, + + stop() { + removePreSendListener(this.preSend); + removePreEditListener(this.preEdit); + } +}); diff --git a/src/plugins/fakeProfileThemes.tsx b/src/plugins/fakeProfileThemes.tsx deleted file mode 100644 index 70003e5..0000000 --- a/src/plugins/fakeProfileThemes.tsx +++ /dev/null @@ -1,145 +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 . -*/ - -// This plugin is a port from Alyxia's Vendetta plugin -import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import { copyWithToast } from "@utils/misc"; -import definePlugin, { OptionType } from "@utils/types"; -import { Button, Forms } from "@webpack/common"; -import { User } from "discord-types/general"; -import virtualMerge from "virtual-merge"; - -interface UserProfile extends User { - themeColors?: Array; -} - -interface Colors { - primary: number; - accent: number; -} - -function encode(primary: number, accent: number): string { - const message = `[#${primary.toString(16).padStart(6, "0")},#${accent.toString(16).padStart(6, "0")}]`; - const padding = ""; - const encoded = Array.from(message) - .map(x => x.codePointAt(0)) - .filter(x => x! >= 0x20 && x! <= 0x7f) - .map(x => String.fromCodePoint(x! + 0xe0000)) - .join(""); - - return (padding || "") + " " + encoded; -} - -// Courtesy of Cynthia. -function decode(bio: string): Array | null { - if (bio == null) return null; - - const colorString = bio.match( - /\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u, - ); - if (colorString != null) { - const parsed = [...colorString[0]] - .map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000)) - .join(""); - const colors = parsed - .substring(1, parsed.length - 1) - .split(",") - .map(x => parseInt(x.replace("#", "0x"), 16)); - - return colors; - } else { - return null; - } -} - -const settings = definePluginSettings({ - nitroFirst: { - description: "Default color source if both are present", - type: OptionType.SELECT, - options: [ - { label: "Nitro colors", value: true, default: true }, - { label: "Fake colors", value: false }, - ] - } -}); - -export default definePlugin({ - name: "FakeProfileThemes", - description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding", - authors: [Devs.Alyxia, Devs.Remty], - patches: [ - { - find: "getUserProfile=", - replacement: { - match: /(?<=getUserProfile=function\(\i\){return )(\i\[\i\])/, - replace: "$self.colorDecodeHook($1)" - } - }, { - find: ".USER_SETTINGS_PROFILE_THEME_ACCENT", - replacement: { - match: /RESET_PROFILE_THEME}\)(?<=},color:(\i).+?},color:(\i).+?)/, - replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})" - } - } - ], - settingsAboutComponent: () => ( - - Usage - - After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins.
- To set your own colors: -
    -
  • • go to your profile settings
  • -
  • • choose your own colors in the Nitro preview
  • -
  • • click the "Copy 3y3" button
  • -
  • • paste the invisible text anywhere in your bio
  • -

- Please note: if you are using a theme which hides nitro ads, you should disable it temporarily to set colors. -
-
), - settings, - colorDecodeHook(user: UserProfile) { - if (user) { - // don't replace colors if already set with nitro - if (settings.store.nitroFirst && user.themeColors) return user; - const colors = decode(user.bio); - if (colors) { - return virtualMerge(user, { - premiumType: 2, - themeColors: colors - }); - } - } - return user; - }, - addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) { - return ; - }, { noop: true }), -}); diff --git a/src/plugins/fakeProfileThemes/index.tsx b/src/plugins/fakeProfileThemes/index.tsx new file mode 100644 index 0000000..70003e5 --- /dev/null +++ b/src/plugins/fakeProfileThemes/index.tsx @@ -0,0 +1,145 @@ +/* + * 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 . +*/ + +// This plugin is a port from Alyxia's Vendetta plugin +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { Margins } from "@utils/margins"; +import { copyWithToast } from "@utils/misc"; +import definePlugin, { OptionType } from "@utils/types"; +import { Button, Forms } from "@webpack/common"; +import { User } from "discord-types/general"; +import virtualMerge from "virtual-merge"; + +interface UserProfile extends User { + themeColors?: Array; +} + +interface Colors { + primary: number; + accent: number; +} + +function encode(primary: number, accent: number): string { + const message = `[#${primary.toString(16).padStart(6, "0")},#${accent.toString(16).padStart(6, "0")}]`; + const padding = ""; + const encoded = Array.from(message) + .map(x => x.codePointAt(0)) + .filter(x => x! >= 0x20 && x! <= 0x7f) + .map(x => String.fromCodePoint(x! + 0xe0000)) + .join(""); + + return (padding || "") + " " + encoded; +} + +// Courtesy of Cynthia. +function decode(bio: string): Array | null { + if (bio == null) return null; + + const colorString = bio.match( + /\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u, + ); + if (colorString != null) { + const parsed = [...colorString[0]] + .map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000)) + .join(""); + const colors = parsed + .substring(1, parsed.length - 1) + .split(",") + .map(x => parseInt(x.replace("#", "0x"), 16)); + + return colors; + } else { + return null; + } +} + +const settings = definePluginSettings({ + nitroFirst: { + description: "Default color source if both are present", + type: OptionType.SELECT, + options: [ + { label: "Nitro colors", value: true, default: true }, + { label: "Fake colors", value: false }, + ] + } +}); + +export default definePlugin({ + name: "FakeProfileThemes", + description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding", + authors: [Devs.Alyxia, Devs.Remty], + patches: [ + { + find: "getUserProfile=", + replacement: { + match: /(?<=getUserProfile=function\(\i\){return )(\i\[\i\])/, + replace: "$self.colorDecodeHook($1)" + } + }, { + find: ".USER_SETTINGS_PROFILE_THEME_ACCENT", + replacement: { + match: /RESET_PROFILE_THEME}\)(?<=},color:(\i).+?},color:(\i).+?)/, + replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})" + } + } + ], + settingsAboutComponent: () => ( + + Usage + + After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins.
+ To set your own colors: +
    +
  • • go to your profile settings
  • +
  • • choose your own colors in the Nitro preview
  • +
  • • click the "Copy 3y3" button
  • +
  • • paste the invisible text anywhere in your bio
  • +

+ Please note: if you are using a theme which hides nitro ads, you should disable it temporarily to set colors. +
+
), + settings, + colorDecodeHook(user: UserProfile) { + if (user) { + // don't replace colors if already set with nitro + if (settings.store.nitroFirst && user.themeColors) return user; + const colors = decode(user.bio); + if (colors) { + return virtualMerge(user, { + premiumType: 2, + themeColors: colors + }); + } + } + return user; + }, + addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) { + return ; + }, { noop: true }), +}); diff --git a/src/plugins/fixSpotifyEmbeds.desktop.ts b/src/plugins/fixSpotifyEmbeds.desktop.ts deleted file mode 100644 index 4419912..0000000 --- a/src/plugins/fixSpotifyEmbeds.desktop.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { makeRange } from "@components/PluginSettings/components"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -// The entire code of this plugin can be found in ipcPlugins -export default definePlugin({ - name: "FixSpotifyEmbeds", - description: "Fixes spotify embeds being incredibly loud by letting you customise the volume", - authors: [Devs.Ven], - settings: definePluginSettings({ - volume: { - type: OptionType.SLIDER, - description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud", - markers: makeRange(0, 100, 10), - stickToMarkers: false, - default: 10 - } - }) -}); diff --git a/src/plugins/fixSpotifyEmbeds.desktop/index.ts b/src/plugins/fixSpotifyEmbeds.desktop/index.ts new file mode 100644 index 0000000..4419912 --- /dev/null +++ b/src/plugins/fixSpotifyEmbeds.desktop/index.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 { definePluginSettings } from "@api/Settings"; +import { makeRange } from "@components/PluginSettings/components"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +// The entire code of this plugin can be found in ipcPlugins +export default definePlugin({ + name: "FixSpotifyEmbeds", + description: "Fixes spotify embeds being incredibly loud by letting you customise the volume", + authors: [Devs.Ven], + settings: definePluginSettings({ + volume: { + type: OptionType.SLIDER, + description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud", + markers: makeRange(0, 100, 10), + stickToMarkers: false, + default: 10 + } + }) +}); diff --git a/src/plugins/forceOwnerCrown.ts b/src/plugins/forceOwnerCrown.ts deleted file mode 100644 index 3122410..0000000 --- a/src/plugins/forceOwnerCrown.ts +++ /dev/null @@ -1,58 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { GuildStore } from "@webpack/common"; - -export default definePlugin({ - name: "ForceOwnerCrown", - description: "Force the owner crown next to usernames even if the server is large.", - authors: [Devs.D3SOX, Devs.Nickyux], - patches: [ - { - // This is the logic where it decides whether to render the owner crown or not - find: ".renderOwner=", - replacement: { - match: /isOwner;return null!=(\w+)?&&/g, - replace: "isOwner;if($self.isGuildOwner(this.props)){$1=true;}return null!=$1&&" - } - }, - ], - isGuildOwner(props) { - // Check if channel is a Group DM, if so return false - if (props?.channel?.type === 3) { - return false; - } - - // guild id is in props twice, fallback if the first is undefined - const guildId = props?.guildId ?? props?.channel?.guild_id; - const userId = props?.user?.id; - - if (guildId && userId) { - const guild = GuildStore.getGuild(guildId); - if (guild) { - return guild.ownerId === userId; - } - console.error("[ForceOwnerCrown] failed to get guild", { guildId, guild, props }); - } else { - console.error("[ForceOwnerCrown] no guildId or userId", { guildId, userId, props }); - } - return false; - }, -}); diff --git a/src/plugins/forceOwnerCrown/index.ts b/src/plugins/forceOwnerCrown/index.ts new file mode 100644 index 0000000..3122410 --- /dev/null +++ b/src/plugins/forceOwnerCrown/index.ts @@ -0,0 +1,58 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { GuildStore } from "@webpack/common"; + +export default definePlugin({ + name: "ForceOwnerCrown", + description: "Force the owner crown next to usernames even if the server is large.", + authors: [Devs.D3SOX, Devs.Nickyux], + patches: [ + { + // This is the logic where it decides whether to render the owner crown or not + find: ".renderOwner=", + replacement: { + match: /isOwner;return null!=(\w+)?&&/g, + replace: "isOwner;if($self.isGuildOwner(this.props)){$1=true;}return null!=$1&&" + } + }, + ], + isGuildOwner(props) { + // Check if channel is a Group DM, if so return false + if (props?.channel?.type === 3) { + return false; + } + + // guild id is in props twice, fallback if the first is undefined + const guildId = props?.guildId ?? props?.channel?.guild_id; + const userId = props?.user?.id; + + if (guildId && userId) { + const guild = GuildStore.getGuild(guildId); + if (guild) { + return guild.ownerId === userId; + } + console.error("[ForceOwnerCrown] failed to get guild", { guildId, guild, props }); + } else { + console.error("[ForceOwnerCrown] no guildId or userId", { guildId, userId, props }); + } + return false; + }, +}); diff --git a/src/plugins/friendInvites.ts b/src/plugins/friendInvites.ts deleted file mode 100644 index 9220998..0000000 --- a/src/plugins/friendInvites.ts +++ /dev/null @@ -1,121 +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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { RestAPI, UserStore } from "@webpack/common"; - -const FriendInvites = findByPropsLazy("createFriendInvite"); -const uuid = findByPropsLazy("v4", "v1"); - -export default definePlugin({ - name: "FriendInvites", - description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).", - authors: [Devs.afn, Devs.Dziurwa], - dependencies: ["CommandsAPI"], - commands: [ - { - name: "create friend invite", - description: "Generates a friend invite link.", - inputType: ApplicationCommandInputType.BOT, - options: [{ - name: "Uses", - description: "How many uses?", - choices: [ - { label: "1", name: "1", value: "1" }, - { label: "5", name: "5", value: "5" } - ], - required: false, - type: ApplicationCommandOptionType.INTEGER - }], - - execute: async (args, ctx) => { - const uses = findOption(args, "Uses", 5); - - if (uses === 1 && !UserStore.getCurrentUser().phone) - return sendBotMessage(ctx.channel.id, { - content: "You need to have a phone number connected to your account to create a friend invite with 1 use!" - }); - - let invite: any; - if (uses === 1) { - const random = uuid.v4(); - const { body: { invite_suggestions } } = await RestAPI.post({ - url: "/friend-finder/find-friends", - body: { - modified_contacts: { - [random]: [1, "", ""] - }, - phone_contact_methods_count: 1 - } - }); - invite = await FriendInvites.createFriendInvite({ - code: invite_suggestions[0][3], - recipient_phone_number_or_email: random, - contact_visibility: 1, - filter_visibilities: [], - filtered_invite_suggestions_index: 1 - }); - } else { - invite = await FriendInvites.createFriendInvite(); - } - - sendBotMessage(ctx.channel.id, { - content: ` - discord.gg/${invite.code} · - Expires: · - Max uses: \`${invite.max_uses}\` - `.trim().replace(/\s+/g, " ") - }); - } - }, - { - name: "view friend invites", - description: "View a list of all generated friend invites.", - inputType: ApplicationCommandInputType.BOT, - execute: async (_, ctx) => { - const invites = await FriendInvites.getAllFriendInvites(); - const friendInviteList = invites.map(i => - ` - _discord.gg/${i.code}_ · - Expires: · - Times used: \`${i.uses}/${i.max_uses}\` - `.trim().replace(/\s+/g, " ") - ); - - sendBotMessage(ctx.channel.id, { - content: friendInviteList.join("\n") || "You have no active friend invites!" - }); - }, - }, - { - name: "revoke friend invites", - description: "Revokes all generated friend invites.", - inputType: ApplicationCommandInputType.BOT, - execute: async (_, ctx) => { - await FriendInvites.revokeFriendInvites(); - - sendBotMessage(ctx.channel.id, { - content: "All friend invites have been revoked." - }); - }, - }, - ] -}); diff --git a/src/plugins/friendInvites/index.ts b/src/plugins/friendInvites/index.ts new file mode 100644 index 0000000..9220998 --- /dev/null +++ b/src/plugins/friendInvites/index.ts @@ -0,0 +1,121 @@ +/* + * 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { RestAPI, UserStore } from "@webpack/common"; + +const FriendInvites = findByPropsLazy("createFriendInvite"); +const uuid = findByPropsLazy("v4", "v1"); + +export default definePlugin({ + name: "FriendInvites", + description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).", + authors: [Devs.afn, Devs.Dziurwa], + dependencies: ["CommandsAPI"], + commands: [ + { + name: "create friend invite", + description: "Generates a friend invite link.", + inputType: ApplicationCommandInputType.BOT, + options: [{ + name: "Uses", + description: "How many uses?", + choices: [ + { label: "1", name: "1", value: "1" }, + { label: "5", name: "5", value: "5" } + ], + required: false, + type: ApplicationCommandOptionType.INTEGER + }], + + execute: async (args, ctx) => { + const uses = findOption(args, "Uses", 5); + + if (uses === 1 && !UserStore.getCurrentUser().phone) + return sendBotMessage(ctx.channel.id, { + content: "You need to have a phone number connected to your account to create a friend invite with 1 use!" + }); + + let invite: any; + if (uses === 1) { + const random = uuid.v4(); + const { body: { invite_suggestions } } = await RestAPI.post({ + url: "/friend-finder/find-friends", + body: { + modified_contacts: { + [random]: [1, "", ""] + }, + phone_contact_methods_count: 1 + } + }); + invite = await FriendInvites.createFriendInvite({ + code: invite_suggestions[0][3], + recipient_phone_number_or_email: random, + contact_visibility: 1, + filter_visibilities: [], + filtered_invite_suggestions_index: 1 + }); + } else { + invite = await FriendInvites.createFriendInvite(); + } + + sendBotMessage(ctx.channel.id, { + content: ` + discord.gg/${invite.code} · + Expires: · + Max uses: \`${invite.max_uses}\` + `.trim().replace(/\s+/g, " ") + }); + } + }, + { + name: "view friend invites", + description: "View a list of all generated friend invites.", + inputType: ApplicationCommandInputType.BOT, + execute: async (_, ctx) => { + const invites = await FriendInvites.getAllFriendInvites(); + const friendInviteList = invites.map(i => + ` + _discord.gg/${i.code}_ · + Expires: · + Times used: \`${i.uses}/${i.max_uses}\` + `.trim().replace(/\s+/g, " ") + ); + + sendBotMessage(ctx.channel.id, { + content: friendInviteList.join("\n") || "You have no active friend invites!" + }); + }, + }, + { + name: "revoke friend invites", + description: "Revokes all generated friend invites.", + inputType: ApplicationCommandInputType.BOT, + execute: async (_, ctx) => { + await FriendInvites.revokeFriendInvites(); + + sendBotMessage(ctx.channel.id, { + content: "All friend invites have been revoked." + }); + }, + }, + ] +}); diff --git a/src/plugins/gifPaste.ts b/src/plugins/gifPaste.ts deleted file mode 100644 index f1dfb2f..0000000 --- a/src/plugins/gifPaste.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Devs } from "@utils/constants"; -import { insertTextIntoChatInputBox } from "@utils/discord"; -import definePlugin from "@utils/types"; -import { filters, mapMangledModuleLazy } from "@webpack"; - -const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', { - close: filters.byCode("activeView:null", "setState") -}); - -export default definePlugin({ - name: "GifPaste", - description: "Makes picking a gif in the gif picker insert a link into the chatbox instead of instantly sending it", - authors: [Devs.Ven], - - patches: [{ - find: ".handleSelectGIF=", - replacement: { - match: /\.handleSelectGIF=function.+?\{/, - replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);" - } - }], - - handleSelect(gif?: { url: string; }) { - if (gif) { - insertTextIntoChatInputBox(gif.url + " "); - ExpressionPickerState.close(); - } - } -}); diff --git a/src/plugins/gifPaste/index.ts b/src/plugins/gifPaste/index.ts new file mode 100644 index 0000000..f1dfb2f --- /dev/null +++ b/src/plugins/gifPaste/index.ts @@ -0,0 +1,47 @@ +/* + * 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 { insertTextIntoChatInputBox } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { filters, mapMangledModuleLazy } from "@webpack"; + +const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', { + close: filters.byCode("activeView:null", "setState") +}); + +export default definePlugin({ + name: "GifPaste", + description: "Makes picking a gif in the gif picker insert a link into the chatbox instead of instantly sending it", + authors: [Devs.Ven], + + patches: [{ + find: ".handleSelectGIF=", + replacement: { + match: /\.handleSelectGIF=function.+?\{/, + replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);" + } + }], + + handleSelect(gif?: { url: string; }) { + if (gif) { + insertTextIntoChatInputBox(gif.url + " "); + ExpressionPickerState.close(); + } + } +}); diff --git a/src/plugins/greetStickerPicker.tsx b/src/plugins/greetStickerPicker.tsx deleted file mode 100644 index 52f70b3..0000000 --- a/src/plugins/greetStickerPicker.tsx +++ /dev/null @@ -1,188 +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 } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { ContextMenu, FluxDispatcher, Menu } from "@webpack/common"; -import { Channel, Message } from "discord-types/general"; - -interface Sticker { - id: string; - format_type: number; - description: string; - name: string; -} - -enum GreetMode { - Greet = "Greet", - NormalMessage = "Message" -} - -const settings = definePluginSettings({ - greetMode: { - type: OptionType.SELECT, - options: [ - { label: "Greet (you can only greet 3 times)", value: GreetMode.Greet, default: true }, - { label: "Normal Message (you can greet spam)", value: GreetMode.NormalMessage } - ], - description: "Choose the greet mode" - } -}).withPrivateSettings<{ - multiGreetChoices?: string[]; - unholyMultiGreetEnabled?: boolean; -}>(); - -const MessageActions = findByPropsLazy("sendGreetMessage"); - -function greet(channel: Channel, message: Message, stickers: string[]) { - const options = MessageActions.getSendMessageOptionsForReply({ - channel, - message, - shouldMention: true, - showMentionToggle: true - }); - - if (settings.store.greetMode === GreetMode.NormalMessage || stickers.length > 1) { - options.stickerIds = stickers; - const msg = { - content: "", - tts: false, - invalidEmojis: [], - validNonShortcutEmojis: [] - }; - - MessageActions._sendMessage(channel.id, msg, options); - } else { - MessageActions.sendGreetMessage(channel.id, stickers[0], options); - } -} - - -function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], message: Message, channel: Channel; }) { - const s = settings.use(["greetMode", "multiGreetChoices"]); - const { greetMode, multiGreetChoices = [] } = s; - - return ( - FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - aria-label="Greet Sticker Picker" - > - - {Object.values(GreetMode).map(mode => ( - s.greetMode = mode} - /> - ))} - - - - - - {stickers.map(sticker => ( - greet(channel, message, [sticker.id])} - /> - ))} - - - {!settings.store.unholyMultiGreetEnabled ? null : ( - <> - - - - {stickers.map(sticker => { - const checked = multiGreetChoices.some(s => s === sticker.id); - - return ( - = 3} - action={() => { - s.multiGreetChoices = checked - ? multiGreetChoices.filter(s => s !== sticker.id) - : [...multiGreetChoices, sticker.id]; - }} - /> - ); - })} - - - greet(channel, message, multiGreetChoices!)} - disabled={multiGreetChoices.length === 0} - /> - - - - )} - - ); -} - -export default definePlugin({ - name: "GreetStickerPicker", - description: "Allows you to use any greet sticker instead of only the random one by right-clicking the 'Wave to say hi!' button", - authors: [Devs.Ven], - - settings, - - patches: [ - { - find: "Messages.WELCOME_CTA_LABEL", - replacement: { - match: /innerClassName:\i\(\).welcomeCTAButton,(?<=%\i\.length;return (\i)\[\i\].+?)/, - replace: "$&onContextMenu:(e)=>$self.pickSticker(e,$1,arguments[0])," - } - } - ], - - pickSticker( - event: React.UIEvent, - stickers: Sticker[], - props: { - channel: Channel, - message: Message; - } - ) { - if (!(props.message as any).deleted) - ContextMenu.open(event, () => ); - } -}); diff --git a/src/plugins/greetStickerPicker/index.tsx b/src/plugins/greetStickerPicker/index.tsx new file mode 100644 index 0000000..52f70b3 --- /dev/null +++ b/src/plugins/greetStickerPicker/index.tsx @@ -0,0 +1,188 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { ContextMenu, FluxDispatcher, Menu } from "@webpack/common"; +import { Channel, Message } from "discord-types/general"; + +interface Sticker { + id: string; + format_type: number; + description: string; + name: string; +} + +enum GreetMode { + Greet = "Greet", + NormalMessage = "Message" +} + +const settings = definePluginSettings({ + greetMode: { + type: OptionType.SELECT, + options: [ + { label: "Greet (you can only greet 3 times)", value: GreetMode.Greet, default: true }, + { label: "Normal Message (you can greet spam)", value: GreetMode.NormalMessage } + ], + description: "Choose the greet mode" + } +}).withPrivateSettings<{ + multiGreetChoices?: string[]; + unholyMultiGreetEnabled?: boolean; +}>(); + +const MessageActions = findByPropsLazy("sendGreetMessage"); + +function greet(channel: Channel, message: Message, stickers: string[]) { + const options = MessageActions.getSendMessageOptionsForReply({ + channel, + message, + shouldMention: true, + showMentionToggle: true + }); + + if (settings.store.greetMode === GreetMode.NormalMessage || stickers.length > 1) { + options.stickerIds = stickers; + const msg = { + content: "", + tts: false, + invalidEmojis: [], + validNonShortcutEmojis: [] + }; + + MessageActions._sendMessage(channel.id, msg, options); + } else { + MessageActions.sendGreetMessage(channel.id, stickers[0], options); + } +} + + +function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], message: Message, channel: Channel; }) { + const s = settings.use(["greetMode", "multiGreetChoices"]); + const { greetMode, multiGreetChoices = [] } = s; + + return ( + FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} + aria-label="Greet Sticker Picker" + > + + {Object.values(GreetMode).map(mode => ( + s.greetMode = mode} + /> + ))} + + + + + + {stickers.map(sticker => ( + greet(channel, message, [sticker.id])} + /> + ))} + + + {!settings.store.unholyMultiGreetEnabled ? null : ( + <> + + + + {stickers.map(sticker => { + const checked = multiGreetChoices.some(s => s === sticker.id); + + return ( + = 3} + action={() => { + s.multiGreetChoices = checked + ? multiGreetChoices.filter(s => s !== sticker.id) + : [...multiGreetChoices, sticker.id]; + }} + /> + ); + })} + + + greet(channel, message, multiGreetChoices!)} + disabled={multiGreetChoices.length === 0} + /> + + + + )} + + ); +} + +export default definePlugin({ + name: "GreetStickerPicker", + description: "Allows you to use any greet sticker instead of only the random one by right-clicking the 'Wave to say hi!' button", + authors: [Devs.Ven], + + settings, + + patches: [ + { + find: "Messages.WELCOME_CTA_LABEL", + replacement: { + match: /innerClassName:\i\(\).welcomeCTAButton,(?<=%\i\.length;return (\i)\[\i\].+?)/, + replace: "$&onContextMenu:(e)=>$self.pickSticker(e,$1,arguments[0])," + } + } + ], + + pickSticker( + event: React.UIEvent, + stickers: Sticker[], + props: { + channel: Channel, + message: Message; + } + ) { + if (!(props.message as any).deleted) + ContextMenu.open(event, () => ); + } +}); diff --git a/src/plugins/hideAttachments.tsx b/src/plugins/hideAttachments.tsx deleted file mode 100644 index fe7f4ab..0000000 --- a/src/plugins/hideAttachments.tsx +++ /dev/null @@ -1,95 +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 { get, set } from "@api/DataStore"; -import { addButton, removeButton } from "@api/MessagePopover"; -import { ImageInvisible, ImageVisible } from "@components/Icons"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { ChannelStore } from "@webpack/common"; - -let style: HTMLStyleElement; - -const KEY = "HideAttachments_HiddenIds"; - -let hiddenMessages: Set = new Set(); -const getHiddenMessages = () => get(KEY).then(set => { - hiddenMessages = set ?? new Set(); - return hiddenMessages; -}); -const saveHiddenMessages = (ids: Set) => set(KEY, ids); - -export default definePlugin({ - name: "HideAttachments", - description: "Hide attachments and Embeds for individual messages via hover button", - authors: [Devs.Ven], - dependencies: ["MessagePopoverAPI"], - - async start() { - style = document.createElement("style"); - style.id = "VencordHideAttachments"; - document.head.appendChild(style); - - await getHiddenMessages(); - await this.buildCss(); - - addButton("HideAttachments", msg => { - if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null; - - const isHidden = hiddenMessages.has(msg.id); - - return { - label: isHidden ? "Show Attachments" : "Hide Attachments", - icon: isHidden ? ImageVisible : ImageInvisible, - message: msg, - channel: ChannelStore.getChannel(msg.channel_id), - onClick: () => this.toggleHide(msg.id) - }; - }); - }, - - stop() { - style.remove(); - hiddenMessages.clear(); - removeButton("HideAttachments"); - }, - - async buildCss() { - const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(","); - style.textContent = ` - :is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) { - /* important is not necessary, but add it to make sure bad themes won't break it */ - display: none !important; - } - :is(${elements})::after { - content: "Attachments hidden"; - color: var(--text-muted); - font-size: 80%; - } - `; - }, - - async toggleHide(id: string) { - const ids = await getHiddenMessages(); - if (!ids.delete(id)) - ids.add(id); - - await saveHiddenMessages(ids); - await this.buildCss(); - } -}); diff --git a/src/plugins/hideAttachments/index.tsx b/src/plugins/hideAttachments/index.tsx new file mode 100644 index 0000000..fe7f4ab --- /dev/null +++ b/src/plugins/hideAttachments/index.tsx @@ -0,0 +1,95 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { get, set } from "@api/DataStore"; +import { addButton, removeButton } from "@api/MessagePopover"; +import { ImageInvisible, ImageVisible } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { ChannelStore } from "@webpack/common"; + +let style: HTMLStyleElement; + +const KEY = "HideAttachments_HiddenIds"; + +let hiddenMessages: Set = new Set(); +const getHiddenMessages = () => get(KEY).then(set => { + hiddenMessages = set ?? new Set(); + return hiddenMessages; +}); +const saveHiddenMessages = (ids: Set) => set(KEY, ids); + +export default definePlugin({ + name: "HideAttachments", + description: "Hide attachments and Embeds for individual messages via hover button", + authors: [Devs.Ven], + dependencies: ["MessagePopoverAPI"], + + async start() { + style = document.createElement("style"); + style.id = "VencordHideAttachments"; + document.head.appendChild(style); + + await getHiddenMessages(); + await this.buildCss(); + + addButton("HideAttachments", msg => { + if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null; + + const isHidden = hiddenMessages.has(msg.id); + + return { + label: isHidden ? "Show Attachments" : "Hide Attachments", + icon: isHidden ? ImageVisible : ImageInvisible, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => this.toggleHide(msg.id) + }; + }); + }, + + stop() { + style.remove(); + hiddenMessages.clear(); + removeButton("HideAttachments"); + }, + + async buildCss() { + const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(","); + style.textContent = ` + :is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) { + /* important is not necessary, but add it to make sure bad themes won't break it */ + display: none !important; + } + :is(${elements})::after { + content: "Attachments hidden"; + color: var(--text-muted); + font-size: 80%; + } + `; + }, + + async toggleHide(id: string) { + const ids = await getHiddenMessages(); + if (!ids.delete(id)) + ids.add(id); + + await saveHiddenMessages(ids); + await this.buildCss(); + } +}); diff --git a/src/plugins/iLoveSpam.ts b/src/plugins/iLoveSpam.ts deleted file mode 100644 index 79b2e3f..0000000 --- a/src/plugins/iLoveSpam.ts +++ /dev/null @@ -1,35 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "iLoveSpam", - description: "Do not hide messages from 'likely spammers'", - authors: [Devs.botato, Devs.Animal], - patches: [ - { - find: "),{hasFlag:", - replacement: { - match: /(if\((.{1,2})<=1<<30\)return)/, - replace: "if($2===(1<<20)){return false};$1", - }, - }, - ], -}); diff --git a/src/plugins/iLoveSpam/index.ts b/src/plugins/iLoveSpam/index.ts new file mode 100644 index 0000000..79b2e3f --- /dev/null +++ b/src/plugins/iLoveSpam/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "iLoveSpam", + description: "Do not hide messages from 'likely spammers'", + authors: [Devs.botato, Devs.Animal], + patches: [ + { + find: "),{hasFlag:", + replacement: { + match: /(if\((.{1,2})<=1<<30\)return)/, + replace: "if($2===(1<<20)){return false};$1", + }, + }, + ], +}); diff --git a/src/plugins/ignoreActivities.tsx b/src/plugins/ignoreActivities.tsx deleted file mode 100644 index 6d58eb4..0000000 --- a/src/plugins/ignoreActivities.tsx +++ /dev/null @@ -1,233 +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 * as DataStore from "@api/DataStore"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { useForceUpdater } from "@utils/react"; -import definePlugin from "@utils/types"; -import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { Tooltip } from "webpack/common"; - -const enum ActivitiesTypes { - Game, - Embedded -} - -interface IgnoredActivity { - id: string; - type: ActivitiesTypes; -} - -const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn"); -const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon"); -const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight"); -const RunningGameStore = findStoreLazy("RunningGameStore"); - -function ToggleIconOff() { - return ( - - - - - - - ); -} - -function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) { - return ( - - - - ); -} - -function ToggleActivityComponent({ activity, forceWhite, forceLeftMargin }: { activity: IgnoredActivity; forceWhite?: boolean; forceLeftMargin?: boolean; }) { - const forceUpdate = useForceUpdater(); - - return ( - - {({ onMouseLeave, onMouseEnter }) => ( -
handleActivityToggle(e, activity, forceUpdate)} - > - { - ignoredActivitiesCache.has(activity.id) - ? - : - } -
- )} -
- ); -} - -function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) { - return ( -
- -
- ); -} - -function handleActivityToggle(e: React.MouseEvent, activity: IgnoredActivity, forceUpdateComponent: () => void) { - e.stopPropagation(); - if (ignoredActivitiesCache.has(activity.id)) ignoredActivitiesCache.delete(activity.id); - else ignoredActivitiesCache.set(activity.id, activity); - forceUpdateComponent(); - saveCacheToDatastore(); -} - -async function saveCacheToDatastore() { - await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache); -} - -let ignoredActivitiesCache = new Map(); - -export default definePlugin({ - name: "IgnoreActivities", - authors: [Devs.Nuckyz], - description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.", - patches: [ - { - find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", - replacement: { - match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/, - replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false" - + `${restWithoutPlatformCheck}` - + `(${platformCheck}?${children}:[])` - + `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))` - } - }, - { - find: ".overlayBadge", - replacement: [ - { - match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i)\.name.+?null/, - replace: (m, props) => `[${m},$self.renderToggleActivityButton(${props})]` - }, - { - match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i\.application)\.name.+?null/, - replace: (m, props) => `${m},$self.renderToggleActivityButton(${props})` - } - ] - }, - { - find: '.displayName="LocalActivityStore"', - replacement: { - match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/, - replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);` - } - } - ], - - async start() { - const ignoredActivitiesData = await DataStore.get>("IgnoreActivities_ignoredActivities") ?? new Map(); - /** Migrate old data */ - if (Array.isArray(ignoredActivitiesData)) { - for (const id of ignoredActivitiesData) { - ignoredActivitiesCache.set(id, { id, type: ActivitiesTypes.Game }); - } - - await saveCacheToDatastore(); - } else ignoredActivitiesCache = ignoredActivitiesData; - - if (ignoredActivitiesCache.size !== 0) { - const gamesSeen: { id?: string; exePath: string; }[] = RunningGameStore.getGamesSeen(); - - for (const ignoredActivity of ignoredActivitiesCache.values()) { - if (ignoredActivity.type !== ActivitiesTypes.Game) continue; - - if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) { - /** Custom added game which no longer exists */ - ignoredActivitiesCache.delete(ignoredActivity.id); - } - } - - await saveCacheToDatastore(); - } - }, - - renderToggleGameActivityButton(props: { id?: string; exePath: string; }) { - return ( - - - - ); - }, - - renderToggleActivityButton(props: { id: string; }) { - return ( - - - - ); - }, - - isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) { - if (props.type === 0) { - if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id); - else { - const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; - if (exePath) return !ignoredActivitiesCache.has(exePath); - } - } - return true; - } -}); diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx new file mode 100644 index 0000000..6d58eb4 --- /dev/null +++ b/src/plugins/ignoreActivities/index.tsx @@ -0,0 +1,233 @@ +/* + * 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 * as DataStore from "@api/DataStore"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { useForceUpdater } from "@utils/react"; +import definePlugin from "@utils/types"; +import { findByPropsLazy, findStoreLazy } from "@webpack"; +import { Tooltip } from "webpack/common"; + +const enum ActivitiesTypes { + Game, + Embedded +} + +interface IgnoredActivity { + id: string; + type: ActivitiesTypes; +} + +const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn"); +const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon"); +const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight"); +const RunningGameStore = findStoreLazy("RunningGameStore"); + +function ToggleIconOff() { + return ( + + + + + + + ); +} + +function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) { + return ( + + + + ); +} + +function ToggleActivityComponent({ activity, forceWhite, forceLeftMargin }: { activity: IgnoredActivity; forceWhite?: boolean; forceLeftMargin?: boolean; }) { + const forceUpdate = useForceUpdater(); + + return ( + + {({ onMouseLeave, onMouseEnter }) => ( +
handleActivityToggle(e, activity, forceUpdate)} + > + { + ignoredActivitiesCache.has(activity.id) + ? + : + } +
+ )} +
+ ); +} + +function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) { + return ( +
+ +
+ ); +} + +function handleActivityToggle(e: React.MouseEvent, activity: IgnoredActivity, forceUpdateComponent: () => void) { + e.stopPropagation(); + if (ignoredActivitiesCache.has(activity.id)) ignoredActivitiesCache.delete(activity.id); + else ignoredActivitiesCache.set(activity.id, activity); + forceUpdateComponent(); + saveCacheToDatastore(); +} + +async function saveCacheToDatastore() { + await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache); +} + +let ignoredActivitiesCache = new Map(); + +export default definePlugin({ + name: "IgnoreActivities", + authors: [Devs.Nuckyz], + description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.", + patches: [ + { + find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", + replacement: { + match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/, + replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false" + + `${restWithoutPlatformCheck}` + + `(${platformCheck}?${children}:[])` + + `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))` + } + }, + { + find: ".overlayBadge", + replacement: [ + { + match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i)\.name.+?null/, + replace: (m, props) => `[${m},$self.renderToggleActivityButton(${props})]` + }, + { + match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i\.application)\.name.+?null/, + replace: (m, props) => `${m},$self.renderToggleActivityButton(${props})` + } + ] + }, + { + find: '.displayName="LocalActivityStore"', + replacement: { + match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/, + replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);` + } + } + ], + + async start() { + const ignoredActivitiesData = await DataStore.get>("IgnoreActivities_ignoredActivities") ?? new Map(); + /** Migrate old data */ + if (Array.isArray(ignoredActivitiesData)) { + for (const id of ignoredActivitiesData) { + ignoredActivitiesCache.set(id, { id, type: ActivitiesTypes.Game }); + } + + await saveCacheToDatastore(); + } else ignoredActivitiesCache = ignoredActivitiesData; + + if (ignoredActivitiesCache.size !== 0) { + const gamesSeen: { id?: string; exePath: string; }[] = RunningGameStore.getGamesSeen(); + + for (const ignoredActivity of ignoredActivitiesCache.values()) { + if (ignoredActivity.type !== ActivitiesTypes.Game) continue; + + if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) { + /** Custom added game which no longer exists */ + ignoredActivitiesCache.delete(ignoredActivity.id); + } + } + + await saveCacheToDatastore(); + } + }, + + renderToggleGameActivityButton(props: { id?: string; exePath: string; }) { + return ( + + + + ); + }, + + renderToggleActivityButton(props: { id: string; }) { + return ( + + + + ); + }, + + isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) { + if (props.type === 0) { + if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id); + else { + const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; + if (exePath) return !ignoredActivitiesCache.has(exePath); + } + } + return true; + } +}); diff --git a/src/plugins/keepCurrentChannel.ts b/src/plugins/keepCurrentChannel.ts deleted file mode 100644 index b226c34..0000000 --- a/src/plugins/keepCurrentChannel.ts +++ /dev/null @@ -1,90 +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 * as DataStore from "@api/DataStore"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { ChannelStore, NavigationRouter, SelectedChannelStore, SelectedGuildStore } from "@webpack/common"; - -export interface LogoutEvent { - type: "LOGOUT"; - isSwitchingAccount: boolean; -} - -interface ChannelSelectEvent { - type: "CHANNEL_SELECT"; - channelId: string | null; - guildId: string | null; -} - -interface PreviousChannel { - guildId: string | null; - channelId: string | null; -} - -let isSwitchingAccount = false; -let previousCache: PreviousChannel | undefined; - -function attemptToNavigateToChannel(guildId: string | null, channelId: string) { - if (!ChannelStore.hasChannel(channelId)) return; - NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${channelId}`); -} - -export default definePlugin({ - name: "KeepCurrentChannel", - description: "Attempt to navigate to the channel you were in before switching accounts or loading Discord.", - authors: [Devs.Nuckyz], - - flux: { - LOGOUT(e: LogoutEvent) { - ({ isSwitchingAccount } = e); - }, - - CONNECTION_OPEN() { - if (!isSwitchingAccount) return; - isSwitchingAccount = false; - - if (previousCache?.channelId) - attemptToNavigateToChannel(previousCache.guildId, previousCache.channelId); - }, - - async CHANNEL_SELECT({ guildId, channelId }: ChannelSelectEvent) { - if (isSwitchingAccount) return; - - previousCache = { - guildId, - channelId - }; - await DataStore.set("KeepCurrentChannel_previousData", previousCache); - } - }, - - async start() { - previousCache = await DataStore.get("KeepCurrentChannel_previousData"); - if (!previousCache) { - previousCache = { - guildId: SelectedGuildStore.getGuildId(), - channelId: SelectedChannelStore.getChannelId() ?? null - }; - - await DataStore.set("KeepCurrentChannel_previousData", previousCache); - } else if (previousCache.channelId) { - attemptToNavigateToChannel(previousCache.guildId, previousCache.channelId); - } - } -}); diff --git a/src/plugins/keepCurrentChannel/index.ts b/src/plugins/keepCurrentChannel/index.ts new file mode 100644 index 0000000..b226c34 --- /dev/null +++ b/src/plugins/keepCurrentChannel/index.ts @@ -0,0 +1,90 @@ +/* + * 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 * as DataStore from "@api/DataStore"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { ChannelStore, NavigationRouter, SelectedChannelStore, SelectedGuildStore } from "@webpack/common"; + +export interface LogoutEvent { + type: "LOGOUT"; + isSwitchingAccount: boolean; +} + +interface ChannelSelectEvent { + type: "CHANNEL_SELECT"; + channelId: string | null; + guildId: string | null; +} + +interface PreviousChannel { + guildId: string | null; + channelId: string | null; +} + +let isSwitchingAccount = false; +let previousCache: PreviousChannel | undefined; + +function attemptToNavigateToChannel(guildId: string | null, channelId: string) { + if (!ChannelStore.hasChannel(channelId)) return; + NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${channelId}`); +} + +export default definePlugin({ + name: "KeepCurrentChannel", + description: "Attempt to navigate to the channel you were in before switching accounts or loading Discord.", + authors: [Devs.Nuckyz], + + flux: { + LOGOUT(e: LogoutEvent) { + ({ isSwitchingAccount } = e); + }, + + CONNECTION_OPEN() { + if (!isSwitchingAccount) return; + isSwitchingAccount = false; + + if (previousCache?.channelId) + attemptToNavigateToChannel(previousCache.guildId, previousCache.channelId); + }, + + async CHANNEL_SELECT({ guildId, channelId }: ChannelSelectEvent) { + if (isSwitchingAccount) return; + + previousCache = { + guildId, + channelId + }; + await DataStore.set("KeepCurrentChannel_previousData", previousCache); + } + }, + + async start() { + previousCache = await DataStore.get("KeepCurrentChannel_previousData"); + if (!previousCache) { + previousCache = { + guildId: SelectedGuildStore.getGuildId(), + channelId: SelectedChannelStore.getChannelId() ?? null + }; + + await DataStore.set("KeepCurrentChannel_previousData", previousCache); + } else if (previousCache.channelId) { + attemptToNavigateToChannel(previousCache.guildId, previousCache.channelId); + } + } +}); diff --git a/src/plugins/lastfm.tsx b/src/plugins/lastfm.tsx deleted file mode 100644 index 7a42f8f..0000000 --- a/src/plugins/lastfm.tsx +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Sofia Lima - * - * 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 { Link } from "@components/Link"; -import { Devs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import definePlugin, { OptionType } from "@utils/types"; -import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; -import { FluxDispatcher, Forms } from "@webpack/common"; - -interface ActivityAssets { - large_image?: string; - large_text?: string; - small_image?: string; - small_text?: string; -} - - -interface ActivityButton { - label: string; - url: string; -} - -interface Activity { - state: string; - details?: string; - timestamps?: { - start?: number; - }; - assets?: ActivityAssets; - buttons?: Array; - name: string; - application_id: string; - metadata?: { - button_urls?: Array; - }; - type: number; - flags: number; -} - -interface TrackData { - name: string; - album: string; - artist: string; - url: string; - imageUrl?: string; -} - -// only relevant enum values -const enum ActivityType { - PLAYING = 0, - LISTENING = 2, -} - -const enum ActivityFlag { - INSTANCE = 1 << 0, -} - -const enum NameFormat { - StatusName = "status-name", - ArtistFirst = "artist-first", - SongFirst = "song-first", - ArtistOnly = "artist", - SongOnly = "song" -} - -const applicationId = "1108588077900898414"; -const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f"; - -const logger = new Logger("LastFMRichPresence"); - -const presenceStore = findByPropsLazy("getLocalPresence"); -const assetManager = mapMangledModuleLazy( - "getAssetImage: size must === [number, number] for Twitch", - { - getAsset: filters.byCode("apply("), - } -); - -async function getApplicationAsset(key: string): Promise { - return (await assetManager.getAsset(applicationId, [key, undefined]))[0]; -} - -function setActivity(activity: Activity | null) { - FluxDispatcher.dispatch({ - type: "LOCAL_ACTIVITY_UPDATE", - activity, - socketId: "LastFM", - }); -} - -const settings = definePluginSettings({ - username: { - description: "last.fm username", - type: OptionType.STRING, - }, - apiKey: { - description: "last.fm api key", - type: OptionType.STRING, - }, - shareUsername: { - description: "show link to last.fm profile", - type: OptionType.BOOLEAN, - default: false, - }, - hideWithSpotify: { - description: "hide last.fm presence if spotify is running", - type: OptionType.BOOLEAN, - default: true, - }, - statusName: { - description: "custom status text", - type: OptionType.STRING, - default: "some music", - }, - nameFormat: { - description: "Show name of song and artist in status name", - type: OptionType.SELECT, - options: [ - { - label: "Use custom status name", - value: NameFormat.StatusName, - default: true - }, - { - label: "Use format 'artist - song'", - value: NameFormat.ArtistFirst - }, - { - label: "Use format 'song - artist'", - value: NameFormat.SongFirst - }, - { - label: "Use artist name only", - value: NameFormat.ArtistOnly - }, - { - label: "Use song name only", - value: NameFormat.SongOnly - } - ], - }, - useListeningStatus: { - description: 'show "Listening to" status instead of "Playing"', - type: OptionType.BOOLEAN, - default: false, - }, - missingArt: { - description: "When album or album art is missing", - type: OptionType.SELECT, - options: [ - { - label: "Use large Last.fm logo", - value: "lastfmLogo", - default: true - }, - { - label: "Use generic placeholder", - value: "placeholder" - } - ], - }, -}); - -export default definePlugin({ - name: "LastFMRichPresence", - description: "Little plugin for Last.fm rich presence", - authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu], - - settingsAboutComponent: () => ( - <> - How to get an API key - - An API key is required to fetch your current track. To get one, you can - visit this page and - fill in the following information:

- - Application name: Discord Rich Presence
- Application description: (personal use)

- - And copy the API key (not the shared secret!) -
- - ), - - settings, - - start() { - this.updatePresence(); - this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000); - }, - - stop() { - clearInterval(this.updateInterval); - }, - - async fetchTrackData(): Promise { - if (!settings.store.username || !settings.store.apiKey) - return null; - - try { - const params = new URLSearchParams({ - method: "user.getrecenttracks", - api_key: settings.store.apiKey, - user: settings.store.username, - limit: "1", - format: "json" - }); - - const res = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); - if (!res.ok) throw `${res.status} ${res.statusText}`; - - const json = await res.json(); - if (json.error) { - logger.error("Error from Last.fm API", `${json.error}: ${json.message}`); - return null; - } - - const trackData = json.recenttracks?.track[0]; - - if (!trackData?.["@attr"]?.nowplaying) - return null; - - // why does the json api have xml structure - return { - name: trackData.name || "Unknown", - album: trackData.album["#text"], - artist: trackData.artist["#text"] || "Unknown", - url: trackData.url, - imageUrl: trackData.image?.find((x: any) => x.size === "large")?.["#text"] - }; - } catch (e) { - logger.error("Failed to query Last.fm API", e); - // will clear the rich presence if API fails - return null; - } - }, - - async updatePresence() { - setActivity(await this.getActivity()); - }, - - getLargeImage(track: TrackData): string | undefined { - if (track.imageUrl && !track.imageUrl.includes(placeholderId)) - return track.imageUrl; - - if (settings.store.missingArt === "placeholder") - return "placeholder"; - }, - - async getActivity(): Promise { - if (settings.store.hideWithSpotify) { - for (const activity of presenceStore.getActivities()) { - if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) { - // there is already music status because of Spotify or richerCider (probably more) - return null; - } - } - } - - const trackData = await this.fetchTrackData(); - if (!trackData) return null; - - const largeImage = this.getLargeImage(trackData); - const assets: ActivityAssets = largeImage ? - { - large_image: await getApplicationAsset(largeImage), - large_text: trackData.album || undefined, - small_image: await getApplicationAsset("lastfm-small"), - small_text: "Last.fm", - } : { - large_image: await getApplicationAsset("lastfm-large"), - large_text: trackData.album || undefined, - }; - - const buttons: ActivityButton[] = [ - { - label: "View Song", - url: trackData.url, - }, - ]; - - if (settings.store.shareUsername) - buttons.push({ - label: "Last.fm Profile", - url: `https://www.last.fm/user/${settings.store.username}`, - }); - - const statusName = (() => { - switch (settings.store.nameFormat) { - case NameFormat.ArtistFirst: - return trackData.artist + " - " + trackData.name; - case NameFormat.SongFirst: - return trackData.name + " - " + trackData.artist; - case NameFormat.ArtistOnly: - return trackData.artist; - case NameFormat.SongOnly: - return trackData.name; - default: - return settings.store.statusName; - } - })(); - - return { - application_id: applicationId, - name: statusName, - - details: trackData.name, - state: trackData.artist, - assets, - - buttons: buttons.map(v => v.label), - metadata: { - button_urls: buttons.map(v => v.url), - }, - - type: settings.store.useListeningStatus ? ActivityType.LISTENING : ActivityType.PLAYING, - flags: ActivityFlag.INSTANCE, - }; - } -}); diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx new file mode 100644 index 0000000..7a42f8f --- /dev/null +++ b/src/plugins/lastfm/index.tsx @@ -0,0 +1,337 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Sofia Lima + * + * 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 { Link } from "@components/Link"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin, { OptionType } from "@utils/types"; +import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; +import { FluxDispatcher, Forms } from "@webpack/common"; + +interface ActivityAssets { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; +} + + +interface ActivityButton { + label: string; + url: string; +} + +interface Activity { + state: string; + details?: string; + timestamps?: { + start?: number; + }; + assets?: ActivityAssets; + buttons?: Array; + name: string; + application_id: string; + metadata?: { + button_urls?: Array; + }; + type: number; + flags: number; +} + +interface TrackData { + name: string; + album: string; + artist: string; + url: string; + imageUrl?: string; +} + +// only relevant enum values +const enum ActivityType { + PLAYING = 0, + LISTENING = 2, +} + +const enum ActivityFlag { + INSTANCE = 1 << 0, +} + +const enum NameFormat { + StatusName = "status-name", + ArtistFirst = "artist-first", + SongFirst = "song-first", + ArtistOnly = "artist", + SongOnly = "song" +} + +const applicationId = "1108588077900898414"; +const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f"; + +const logger = new Logger("LastFMRichPresence"); + +const presenceStore = findByPropsLazy("getLocalPresence"); +const assetManager = mapMangledModuleLazy( + "getAssetImage: size must === [number, number] for Twitch", + { + getAsset: filters.byCode("apply("), + } +); + +async function getApplicationAsset(key: string): Promise { + return (await assetManager.getAsset(applicationId, [key, undefined]))[0]; +} + +function setActivity(activity: Activity | null) { + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity, + socketId: "LastFM", + }); +} + +const settings = definePluginSettings({ + username: { + description: "last.fm username", + type: OptionType.STRING, + }, + apiKey: { + description: "last.fm api key", + type: OptionType.STRING, + }, + shareUsername: { + description: "show link to last.fm profile", + type: OptionType.BOOLEAN, + default: false, + }, + hideWithSpotify: { + description: "hide last.fm presence if spotify is running", + type: OptionType.BOOLEAN, + default: true, + }, + statusName: { + description: "custom status text", + type: OptionType.STRING, + default: "some music", + }, + nameFormat: { + description: "Show name of song and artist in status name", + type: OptionType.SELECT, + options: [ + { + label: "Use custom status name", + value: NameFormat.StatusName, + default: true + }, + { + label: "Use format 'artist - song'", + value: NameFormat.ArtistFirst + }, + { + label: "Use format 'song - artist'", + value: NameFormat.SongFirst + }, + { + label: "Use artist name only", + value: NameFormat.ArtistOnly + }, + { + label: "Use song name only", + value: NameFormat.SongOnly + } + ], + }, + useListeningStatus: { + description: 'show "Listening to" status instead of "Playing"', + type: OptionType.BOOLEAN, + default: false, + }, + missingArt: { + description: "When album or album art is missing", + type: OptionType.SELECT, + options: [ + { + label: "Use large Last.fm logo", + value: "lastfmLogo", + default: true + }, + { + label: "Use generic placeholder", + value: "placeholder" + } + ], + }, +}); + +export default definePlugin({ + name: "LastFMRichPresence", + description: "Little plugin for Last.fm rich presence", + authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu], + + settingsAboutComponent: () => ( + <> + How to get an API key + + An API key is required to fetch your current track. To get one, you can + visit this page and + fill in the following information:

+ + Application name: Discord Rich Presence
+ Application description: (personal use)

+ + And copy the API key (not the shared secret!) +
+ + ), + + settings, + + start() { + this.updatePresence(); + this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000); + }, + + stop() { + clearInterval(this.updateInterval); + }, + + async fetchTrackData(): Promise { + if (!settings.store.username || !settings.store.apiKey) + return null; + + try { + const params = new URLSearchParams({ + method: "user.getrecenttracks", + api_key: settings.store.apiKey, + user: settings.store.username, + limit: "1", + format: "json" + }); + + const res = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); + if (!res.ok) throw `${res.status} ${res.statusText}`; + + const json = await res.json(); + if (json.error) { + logger.error("Error from Last.fm API", `${json.error}: ${json.message}`); + return null; + } + + const trackData = json.recenttracks?.track[0]; + + if (!trackData?.["@attr"]?.nowplaying) + return null; + + // why does the json api have xml structure + return { + name: trackData.name || "Unknown", + album: trackData.album["#text"], + artist: trackData.artist["#text"] || "Unknown", + url: trackData.url, + imageUrl: trackData.image?.find((x: any) => x.size === "large")?.["#text"] + }; + } catch (e) { + logger.error("Failed to query Last.fm API", e); + // will clear the rich presence if API fails + return null; + } + }, + + async updatePresence() { + setActivity(await this.getActivity()); + }, + + getLargeImage(track: TrackData): string | undefined { + if (track.imageUrl && !track.imageUrl.includes(placeholderId)) + return track.imageUrl; + + if (settings.store.missingArt === "placeholder") + return "placeholder"; + }, + + async getActivity(): Promise { + if (settings.store.hideWithSpotify) { + for (const activity of presenceStore.getActivities()) { + if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) { + // there is already music status because of Spotify or richerCider (probably more) + return null; + } + } + } + + const trackData = await this.fetchTrackData(); + if (!trackData) return null; + + const largeImage = this.getLargeImage(trackData); + const assets: ActivityAssets = largeImage ? + { + large_image: await getApplicationAsset(largeImage), + large_text: trackData.album || undefined, + small_image: await getApplicationAsset("lastfm-small"), + small_text: "Last.fm", + } : { + large_image: await getApplicationAsset("lastfm-large"), + large_text: trackData.album || undefined, + }; + + const buttons: ActivityButton[] = [ + { + label: "View Song", + url: trackData.url, + }, + ]; + + if (settings.store.shareUsername) + buttons.push({ + label: "Last.fm Profile", + url: `https://www.last.fm/user/${settings.store.username}`, + }); + + const statusName = (() => { + switch (settings.store.nameFormat) { + case NameFormat.ArtistFirst: + return trackData.artist + " - " + trackData.name; + case NameFormat.SongFirst: + return trackData.name + " - " + trackData.artist; + case NameFormat.ArtistOnly: + return trackData.artist; + case NameFormat.SongOnly: + return trackData.name; + default: + return settings.store.statusName; + } + })(); + + return { + application_id: applicationId, + name: statusName, + + details: trackData.name, + state: trackData.artist, + assets, + + buttons: buttons.map(v => v.label), + metadata: { + button_urls: buttons.map(v => v.url), + }, + + type: settings.store.useListeningStatus ? ActivityType.LISTENING : ActivityType.PLAYING, + flags: ActivityFlag.INSTANCE, + }; + } +}); diff --git a/src/plugins/loadingQuotes.ts b/src/plugins/loadingQuotes.ts deleted file mode 100644 index 963705b..0000000 --- a/src/plugins/loadingQuotes.ts +++ /dev/null @@ -1,86 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -// These are Xor encrypted to prevent you from spoiling yourself when you read the source code. -// don't worry about it :P -const quotes = [ - "Eyrokac", - "Rdcg$l`'k|~n", - 'H`tf$d&iajo+d`{"', - "Sucqplh`(Eclhualva()&", - "Lncgmka'8KNMDC,shpanf'`x./,", - "Ioqweijnfn*IeuvfvAotkfxo./,", - 'Hd{#cp\x7Ft$)nbd!{lq%mig~*\x7Fh`v#mk&sm{gx nd#idjb(a\x7Ffao"bja&amdkge!Rloìkhf)hyedfjjb*\'^hzdrdmm$lu\'|ao+mnqw$fijxh~bbmg#Tjmîefd+fnp#lpkffz5', - "h", - "sijklm&cam*rot\"hjjq'|ak\x7F xmv#wc'ep*mawmvvlrb(|ynr>\"Aqq&cgg-\x7F ugoh%rom)e\x7Fhdpp%$", - 'Tnfb}"u\'~`nno!kp$vvhfzeyee"a}%Tfam*Xh`fls%Jboldos-"lj`&hn)~ce!`jcbct|)gdbhnf$wikm$zgaxkmc%afely+og"144?\'ign+iu%p$qisiefr gpfa$', - "Ndtfv%ahfgk+ghtf$|ir(|z' Oguaw&`ggdj mgw$|ir(me|n", - "(!ͣ³$͙ʐ'ͩ¹#", - "(ネ◗ロ◑,マ-2ャユ✬", - "Ynw#hjil(ze+psgwp|&sgmkr!", - "Tikmolh`(fl+a!dvjk\x7F'y|e\x7Fe/,-", - "3/3750?5><9>885:7", - "mdmt", - "Wdn`khc+(oxbeof", - 'Ig"zkp*\'g{*xolglj`&~g|*gowg/$mgt(Eclm`.#ticf{l*xed"wl`&Kangj igbhqn\'d`dn `v#lqrw{3%$bhv-h|)kangj_imwhlhb', - "Tscmw%Tnoa~x", - "I‘f#npus(ec`e!vl$lhsm{`ncu\"ekw&f(defeov-$Rnf|)sdu‘pf$wcam{ceg!vl$du'D`d~x-\"jw%oi(okht-\"DJP)Kags,!mq$du'A‐|n sg`akrkq)~jkdl#pj&diefbnf\"jp)&@F\\*{ltq#Hlhrp'", - "Ynw$v`&cg`dl fml`%rhlhs*", - "Dnl$p%qhz{s' hv$w%hh|aceg!;#gpvt(fl+cndea`&dg|fon&v#wjjqm(", - "\ud83d)pft`gs(ec`e!13$qojmz#", - "a!njcmr'ide~nu\"lb%rheoedldpz$lu'gbkr", - "dn\"zkp&kgo4", - "hnpqkw", - "sn\"fau", - "Sn\"tmqnh}}*musvkaw&flf&+ldv$w%lr{}*aulr#vlao|)cetn\"jp$", - "Dxkmc%ot(hhxomwwai'{hln", - "hd{#}js&(pe~'sg#gprb(3#\"", - "hd{b${", - "<;vqkijbq33271:56<3799?24944:", - "Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&", - "snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno", - "Wdn`khc'|f*eghl{%" -]; - -export default definePlugin({ - name: "LoadingQuotes", - description: "Replace Discords loading quotes", - authors: [Devs.Ven, Devs.KraXen72], - patches: [ - { - find: ".LOADING_DID_YOU_KNOW", - replacement: { - match: /\._loadingText=.+?random\(.+?;/s, - replace: "._loadingText=$self.quote;", - }, - }, - ], - - xor(quote: string) { - const key = "read if cute"; - const codes = Array.from(quote, (s, i) => s.charCodeAt(0) ^ (i % key.length)); - return String.fromCharCode(...codes); - }, - - get quote() { - return this.xor(quotes[Math.floor(Math.random() * quotes.length)]); - } -}); diff --git a/src/plugins/loadingQuotes/index.ts b/src/plugins/loadingQuotes/index.ts new file mode 100644 index 0000000..963705b --- /dev/null +++ b/src/plugins/loadingQuotes/index.ts @@ -0,0 +1,86 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +// These are Xor encrypted to prevent you from spoiling yourself when you read the source code. +// don't worry about it :P +const quotes = [ + "Eyrokac", + "Rdcg$l`'k|~n", + 'H`tf$d&iajo+d`{"', + "Sucqplh`(Eclhualva()&", + "Lncgmka'8KNMDC,shpanf'`x./,", + "Ioqweijnfn*IeuvfvAotkfxo./,", + 'Hd{#cp\x7Ft$)nbd!{lq%mig~*\x7Fh`v#mk&sm{gx nd#idjb(a\x7Ffao"bja&amdkge!Rloìkhf)hyedfjjb*\'^hzdrdmm$lu\'|ao+mnqw$fijxh~bbmg#Tjmîefd+fnp#lpkffz5', + "h", + "sijklm&cam*rot\"hjjq'|ak\x7F xmv#wc'ep*mawmvvlrb(|ynr>\"Aqq&cgg-\x7F ugoh%rom)e\x7Fhdpp%$", + 'Tnfb}"u\'~`nno!kp$vvhfzeyee"a}%Tfam*Xh`fls%Jboldos-"lj`&hn)~ce!`jcbct|)gdbhnf$wikm$zgaxkmc%afely+og"144?\'ign+iu%p$qisiefr gpfa$', + "Ndtfv%ahfgk+ghtf$|ir(|z' Oguaw&`ggdj mgw$|ir(me|n", + "(!ͣ³$͙ʐ'ͩ¹#", + "(ネ◗ロ◑,マ-2ャユ✬", + "Ynw#hjil(ze+psgwp|&sgmkr!", + "Tikmolh`(fl+a!dvjk\x7F'y|e\x7Fe/,-", + "3/3750?5><9>885:7", + "mdmt", + "Wdn`khc+(oxbeof", + 'Ig"zkp*\'g{*xolglj`&~g|*gowg/$mgt(Eclm`.#ticf{l*xed"wl`&Kangj igbhqn\'d`dn `v#lqrw{3%$bhv-h|)kangj_imwhlhb', + "Tscmw%Tnoa~x", + "I‘f#npus(ec`e!vl$lhsm{`ncu\"ekw&f(defeov-$Rnf|)sdu‘pf$wcam{ceg!vl$du'D`d~x-\"jw%oi(okht-\"DJP)Kags,!mq$du'A‐|n sg`akrkq)~jkdl#pj&diefbnf\"jp)&@F\\*{ltq#Hlhrp'", + "Ynw$v`&cg`dl fml`%rhlhs*", + "Dnl$p%qhz{s' hv$w%hh|aceg!;#gpvt(fl+cndea`&dg|fon&v#wjjqm(", + "\ud83d)pft`gs(ec`e!13$qojmz#", + "a!njcmr'ide~nu\"lb%rheoedldpz$lu'gbkr", + "dn\"zkp&kgo4", + "hnpqkw", + "sn\"fau", + "Sn\"tmqnh}}*musvkaw&flf&+ldv$w%lr{}*aulr#vlao|)cetn\"jp$", + "Dxkmc%ot(hhxomwwai'{hln", + "hd{#}js&(pe~'sg#gprb(3#\"", + "hd{b${", + "<;vqkijbq33271:56<3799?24944:", + "Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&", + "snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno", + "Wdn`khc'|f*eghl{%" +]; + +export default definePlugin({ + name: "LoadingQuotes", + description: "Replace Discords loading quotes", + authors: [Devs.Ven, Devs.KraXen72], + patches: [ + { + find: ".LOADING_DID_YOU_KNOW", + replacement: { + match: /\._loadingText=.+?random\(.+?;/s, + replace: "._loadingText=$self.quote;", + }, + }, + ], + + xor(quote: string) { + const key = "read if cute"; + const codes = Array.from(quote, (s, i) => s.charCodeAt(0) ^ (i % key.length)); + return String.fromCharCode(...codes); + }, + + get quote() { + return this.xor(quotes[Math.floor(Math.random() * quotes.length)]); + } +}); diff --git a/src/plugins/memberCount.tsx b/src/plugins/memberCount.tsx deleted file mode 100644 index ecdb8af..0000000 --- a/src/plugins/memberCount.tsx +++ /dev/null @@ -1,116 +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 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 & { - 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 default definePlugin({ - name: "MemberCount", - description: "Shows the amount of online & total members in the server member list", - authors: [Devs.Ven, Devs.Commandtechno], - - patches: [{ - find: ".isSidebarVisible,", - replacement: { - match: /(var (\i)=\i\.className.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, - replace: "$1:[$2?.startsWith('members')?$self.render():null,$3" - } - }], - - render: ErrorBoundary.wrap(MemberCount, { noop: true }) -}); diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx new file mode 100644 index 0000000..ecdb8af --- /dev/null +++ b/src/plugins/memberCount/index.tsx @@ -0,0 +1,116 @@ +/* + * 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 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 & { + 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 default definePlugin({ + name: "MemberCount", + description: "Shows the amount of online & total members in the server member list", + authors: [Devs.Ven, Devs.Commandtechno], + + patches: [{ + find: ".isSidebarVisible,", + replacement: { + match: /(var (\i)=\i\.className.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, + replace: "$1:[$2?.startsWith('members')?$self.render():null,$3" + } + }], + + render: ErrorBoundary.wrap(MemberCount, { noop: true }) +}); diff --git a/src/plugins/messageClickActions.ts b/src/plugins/messageClickActions.ts deleted file mode 100644 index 5fd6a49..0000000 --- a/src/plugins/messageClickActions.ts +++ /dev/null @@ -1,113 +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 { addClickListener, removeClickListener } from "@api/MessageEvents"; -import { definePluginSettings, Settings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { FluxDispatcher, PermissionStore, UserStore } from "@webpack/common"; - -let isDeletePressed = false; -const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true); -const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false); - -const MANAGE_CHANNELS = 1n << 4n; - -const settings = definePluginSettings({ - enableDeleteOnClick: { - type: OptionType.BOOLEAN, - description: "Enable delete on click", - default: true - }, - enableDoubleClickToEdit: { - type: OptionType.BOOLEAN, - description: "Enable double click to edit", - default: true - }, - enableDoubleClickToReply: { - type: OptionType.BOOLEAN, - description: "Enable double click to reply", - default: true - }, - requireModifier: { - type: OptionType.BOOLEAN, - description: "Only do double click actions when shift/ctrl is held", - default: false - } -}); - -export default definePlugin({ - name: "MessageClickActions", - description: "Hold Backspace and click to delete, double click to edit/reply", - authors: [Devs.Ven], - dependencies: ["MessageEventsAPI"], - - settings, - - start() { - const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); - const EditStore = findByPropsLazy("isEditing", "isEditingAny"); - - document.addEventListener("keydown", keydown); - document.addEventListener("keyup", keyup); - - this.onClick = addClickListener((msg: any, channel, event) => { - const isMe = msg.author.id === UserStore.getCurrentUser().id; - if (!isDeletePressed) { - if (event.detail < 2) return; - if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return; - - if (isMe) { - if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id)) return; - - MessageActions.startEditMessage(channel.id, msg.id, msg.content); - event.preventDefault(); - } else { - if (!settings.store.enableDoubleClickToReply) return; - - FluxDispatcher.dispatch({ - type: "CREATE_PENDING_REPLY", - channel, - message: msg, - shouldMention: !Settings.plugins.NoReplyMention.enabled, - showMentionToggle: channel.guild_id !== null - }); - } - } else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(MANAGE_CHANNELS, channel))) { - if (msg.deleted) { - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: channel.id, - id: msg.id, - mlDeleted: true - }); - } else { - MessageActions.deleteMessage(channel.id, msg.id); - } - event.preventDefault(); - } - }); - }, - - stop() { - removeClickListener(this.onClick); - document.removeEventListener("keydown", keydown); - document.removeEventListener("keyup", keyup); - } -}); diff --git a/src/plugins/messageClickActions/index.ts b/src/plugins/messageClickActions/index.ts new file mode 100644 index 0000000..5fd6a49 --- /dev/null +++ b/src/plugins/messageClickActions/index.ts @@ -0,0 +1,113 @@ +/* + * 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 { addClickListener, removeClickListener } from "@api/MessageEvents"; +import { definePluginSettings, Settings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { FluxDispatcher, PermissionStore, UserStore } from "@webpack/common"; + +let isDeletePressed = false; +const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true); +const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false); + +const MANAGE_CHANNELS = 1n << 4n; + +const settings = definePluginSettings({ + enableDeleteOnClick: { + type: OptionType.BOOLEAN, + description: "Enable delete on click", + default: true + }, + enableDoubleClickToEdit: { + type: OptionType.BOOLEAN, + description: "Enable double click to edit", + default: true + }, + enableDoubleClickToReply: { + type: OptionType.BOOLEAN, + description: "Enable double click to reply", + default: true + }, + requireModifier: { + type: OptionType.BOOLEAN, + description: "Only do double click actions when shift/ctrl is held", + default: false + } +}); + +export default definePlugin({ + name: "MessageClickActions", + description: "Hold Backspace and click to delete, double click to edit/reply", + authors: [Devs.Ven], + dependencies: ["MessageEventsAPI"], + + settings, + + start() { + const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); + const EditStore = findByPropsLazy("isEditing", "isEditingAny"); + + document.addEventListener("keydown", keydown); + document.addEventListener("keyup", keyup); + + this.onClick = addClickListener((msg: any, channel, event) => { + const isMe = msg.author.id === UserStore.getCurrentUser().id; + if (!isDeletePressed) { + if (event.detail < 2) return; + if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return; + + if (isMe) { + if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id)) return; + + MessageActions.startEditMessage(channel.id, msg.id, msg.content); + event.preventDefault(); + } else { + if (!settings.store.enableDoubleClickToReply) return; + + FluxDispatcher.dispatch({ + type: "CREATE_PENDING_REPLY", + channel, + message: msg, + shouldMention: !Settings.plugins.NoReplyMention.enabled, + showMentionToggle: channel.guild_id !== null + }); + } + } else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(MANAGE_CHANNELS, channel))) { + if (msg.deleted) { + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: channel.id, + id: msg.id, + mlDeleted: true + }); + } else { + MessageActions.deleteMessage(channel.id, msg.id); + } + event.preventDefault(); + } + }); + }, + + stop() { + removeClickListener(this.onClick); + document.removeEventListener("keydown", keydown); + document.removeEventListener("keyup", keyup); + } +}); diff --git a/src/plugins/messageLinkEmbeds.tsx b/src/plugins/messageLinkEmbeds.tsx deleted file mode 100644 index c7b3bd0..0000000 --- a/src/plugins/messageLinkEmbeds.tsx +++ /dev/null @@ -1,402 +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 { addAccessory } from "@api/MessageAccessories"; -import { definePluginSettings } from "@api/Settings"; -import { getSettingStoreLazy } from "@api/SettingsStore"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants.js"; -import { classes } from "@utils/misc"; -import { Queue } from "@utils/Queue"; -import { LazyComponent } from "@utils/react"; -import definePlugin, { OptionType } from "@utils/types"; -import { find, findByCode, findByPropsLazy } from "@webpack"; -import { - Button, - ChannelStore, - FluxDispatcher, - GuildStore, - MessageStore, - Parser, - PermissionStore, - RestAPI, - Text, - UserStore -} from "@webpack/common"; -import { Channel, Guild, Message } from "discord-types/general"; - -const messageCache = new Map(); - -const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed")); -const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes('["message","compact","className",'))); - -const SearchResultClasses = findByPropsLazy("message", "searchResult"); - -let AutoModEmbed: React.ComponentType = () => null; - -const messageLinkRegex = /(? - - } -}); - - -async function fetchMessage(channelID: string, messageID: string) { - const cached = messageCache.get(messageID); - if (cached) return cached.message; - - messageCache.set(messageID, { fetched: false }); - - const res = await RestAPI.get({ - url: `/channels/${channelID}/messages`, - query: { - limit: 1, - around: messageID - }, - retries: 2 - }).catch(() => null); - - const msg = res?.body?.[0]; - if (!msg) return; - - const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id); - - messageCache.set(message.id, { - message, - fetched: true - }); - - return message; -} - - -function getImages(message: Message): Attachment[] { - const attachments: Attachment[] = []; - - for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) { - if (content_type?.startsWith("image/")) - attachments.push({ - height: height!, - width: width!, - url: url, - proxyURL: proxy_url! - }); - } - - for (const { type, image, thumbnail, url } of message.embeds ?? []) { - if (type === "image") - attachments.push({ ...(image ?? thumbnail!) }); - else if (url && type === "gifv" && !tenorRegex.test(url)) - attachments.push({ - height: thumbnail!.height, - width: thumbnail!.width, - url - }); - } - - return attachments; -} - -function noContent(attachments: number, embeds: number) { - if (!attachments && !embeds) return ""; - if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`; - if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`; - return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`; -} - -function requiresRichEmbed(message: Message) { - if (message.components.length) return true; - if (message.attachments.some(a => !a.content_type?.startsWith("image/"))) return true; - if (message.embeds.some(e => e.type !== "image" && (e.type !== "gifv" || tenorRegex.test(e.url!)))) return true; - - return false; -} - -function computeWidthAndHeight(width: number, height: number) { - const maxWidth = 400; - const maxHeight = 300; - - if (width > height) { - const adjustedWidth = Math.min(width, maxWidth); - return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) }; - } - - const adjustedHeight = Math.min(height, maxHeight); - return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight }; -} - -function withEmbeddedBy(message: Message, embeddedBy: string[]) { - return new Proxy(message, { - get(_, prop) { - if (prop === "vencordEmbeddedBy") return embeddedBy; - // @ts-ignore ts so bad - return Reflect.get(...arguments); - } - }); -} - - -function MessageEmbedAccessory({ message }: { message: Message; }) { - // @ts-ignore - const embeddedBy: string[] = message.vencordEmbeddedBy ?? []; - - const accessories = [] as (JSX.Element | null)[]; - - let match = null as RegExpMatchArray | null; - while ((match = messageLinkRegex.exec(message.content!)) !== null) { - const [_, guildID, channelID, messageID] = match; - if (embeddedBy.includes(messageID)) { - continue; - } - - const linkedChannel = ChannelStore.getChannel(channelID); - if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) { - continue; - } - - const { listMode, idList } = settings.store; - - const isListed = [guildID, channelID, message.author.id].some(id => id && idList.includes(id)); - - if (listMode === "blacklist" && isListed) continue; - if (listMode === "whitelist" && !isListed) continue; - - let linkedMessage = messageCache.get(messageID)?.message; - if (!linkedMessage) { - linkedMessage ??= MessageStore.getMessage(channelID, messageID); - if (linkedMessage) { - messageCache.set(messageID, { message: linkedMessage, fetched: true }); - } else { - const msg = { ...message } as any; - delete msg.embeds; - delete msg.interaction; - - messageFetchQueue.push(() => fetchMessage(channelID, messageID) - .then(m => m && FluxDispatcher.dispatch({ - type: "MESSAGE_UPDATE", - message: msg - })) - ); - continue; - } - } - - const messageProps: MessageEmbedProps = { - message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]), - channel: linkedChannel, - guildID - }; - - const type = settings.store.automodEmbeds; - accessories.push( - type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage)) - ? - : - ); - } - - return accessories.length ? <>{accessories} : null; -} - -function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null { - const isDM = guildID === "@me"; - - const guild = !isDM && GuildStore.getGuild(channel.guild_id); - const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]); - - - return - {isDM ? "Direct Message - " : (guild as Guild).name + " - "} - {isDM - ? Parser.parse(`<@${dmReceiver.id}>`) - : Parser.parse(`<#${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={() => ( -
- -
- )} - />; -} - -const compactModeEnabled = getSettingStoreLazy("textAndImages", "messageDisplayCompact")!; - -function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { - const { message, channel, guildID } = props; - const isDM = guildID === "@me"; - const images = getImages(message); - const { parse } = Parser; - - return - {isDM - ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) - : parse(`<#${channel.id}>`) - } - {isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name} - - } - compact={compactModeEnabled.getSetting()} - content={ - <> - {message.content || message.attachments.length <= images.length - ? parse(message.content) - : [noContent(message.attachments.length, message.embeds.length)] - } - {images.map(a => { - const { width, height } = computeWidthAndHeight(a.width, a.height); - return ( -
- -
- ); - })} - - } - hideTimestamp={false} - message={message} - _messageEmbed="automod" - />; -} - -export default definePlugin({ - name: "MessageLinkEmbeds", - description: "Adds a preview to messages that link another message", - authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev], - dependencies: ["MessageAccessoriesAPI", "SettingsStoreAPI"], - patches: [ - { - find: ".embedCard", - replacement: [{ - match: /function (\i)\(\i\){var \i=\i\.message,\i=\i\.channel.{0,200}\.hideTimestamp/, - replace: "$self.AutoModEmbed=$1;$&" - }] - } - ], - - set AutoModEmbed(e: any) { - AutoModEmbed = e; - }, - - settings, - - start() { - addAccessory("messageLinkEmbed", props => { - if (!messageLinkRegex.test(props.message.content)) - return null; - - // need to reset the regex because it's global - messageLinkRegex.lastIndex = 0; - - return ( - - - - ); - }, 4 /* just above rich embeds */); - }, -}); diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx new file mode 100644 index 0000000..c7b3bd0 --- /dev/null +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -0,0 +1,402 @@ +/* + * 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 { addAccessory } from "@api/MessageAccessories"; +import { definePluginSettings } from "@api/Settings"; +import { getSettingStoreLazy } from "@api/SettingsStore"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants.js"; +import { classes } from "@utils/misc"; +import { Queue } from "@utils/Queue"; +import { LazyComponent } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { find, findByCode, findByPropsLazy } from "@webpack"; +import { + Button, + ChannelStore, + FluxDispatcher, + GuildStore, + MessageStore, + Parser, + PermissionStore, + RestAPI, + Text, + UserStore +} from "@webpack/common"; +import { Channel, Guild, Message } from "discord-types/general"; + +const messageCache = new Map(); + +const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed")); +const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes('["message","compact","className",'))); + +const SearchResultClasses = findByPropsLazy("message", "searchResult"); + +let AutoModEmbed: React.ComponentType = () => null; + +const messageLinkRegex = /(? + + } +}); + + +async function fetchMessage(channelID: string, messageID: string) { + const cached = messageCache.get(messageID); + if (cached) return cached.message; + + messageCache.set(messageID, { fetched: false }); + + const res = await RestAPI.get({ + url: `/channels/${channelID}/messages`, + query: { + limit: 1, + around: messageID + }, + retries: 2 + }).catch(() => null); + + const msg = res?.body?.[0]; + if (!msg) return; + + const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id); + + messageCache.set(message.id, { + message, + fetched: true + }); + + return message; +} + + +function getImages(message: Message): Attachment[] { + const attachments: Attachment[] = []; + + for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) { + if (content_type?.startsWith("image/")) + attachments.push({ + height: height!, + width: width!, + url: url, + proxyURL: proxy_url! + }); + } + + for (const { type, image, thumbnail, url } of message.embeds ?? []) { + if (type === "image") + attachments.push({ ...(image ?? thumbnail!) }); + else if (url && type === "gifv" && !tenorRegex.test(url)) + attachments.push({ + height: thumbnail!.height, + width: thumbnail!.width, + url + }); + } + + return attachments; +} + +function noContent(attachments: number, embeds: number) { + if (!attachments && !embeds) return ""; + if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`; + if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`; + return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`; +} + +function requiresRichEmbed(message: Message) { + if (message.components.length) return true; + if (message.attachments.some(a => !a.content_type?.startsWith("image/"))) return true; + if (message.embeds.some(e => e.type !== "image" && (e.type !== "gifv" || tenorRegex.test(e.url!)))) return true; + + return false; +} + +function computeWidthAndHeight(width: number, height: number) { + const maxWidth = 400; + const maxHeight = 300; + + if (width > height) { + const adjustedWidth = Math.min(width, maxWidth); + return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) }; + } + + const adjustedHeight = Math.min(height, maxHeight); + return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight }; +} + +function withEmbeddedBy(message: Message, embeddedBy: string[]) { + return new Proxy(message, { + get(_, prop) { + if (prop === "vencordEmbeddedBy") return embeddedBy; + // @ts-ignore ts so bad + return Reflect.get(...arguments); + } + }); +} + + +function MessageEmbedAccessory({ message }: { message: Message; }) { + // @ts-ignore + const embeddedBy: string[] = message.vencordEmbeddedBy ?? []; + + const accessories = [] as (JSX.Element | null)[]; + + let match = null as RegExpMatchArray | null; + while ((match = messageLinkRegex.exec(message.content!)) !== null) { + const [_, guildID, channelID, messageID] = match; + if (embeddedBy.includes(messageID)) { + continue; + } + + const linkedChannel = ChannelStore.getChannel(channelID); + if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) { + continue; + } + + const { listMode, idList } = settings.store; + + const isListed = [guildID, channelID, message.author.id].some(id => id && idList.includes(id)); + + if (listMode === "blacklist" && isListed) continue; + if (listMode === "whitelist" && !isListed) continue; + + let linkedMessage = messageCache.get(messageID)?.message; + if (!linkedMessage) { + linkedMessage ??= MessageStore.getMessage(channelID, messageID); + if (linkedMessage) { + messageCache.set(messageID, { message: linkedMessage, fetched: true }); + } else { + const msg = { ...message } as any; + delete msg.embeds; + delete msg.interaction; + + messageFetchQueue.push(() => fetchMessage(channelID, messageID) + .then(m => m && FluxDispatcher.dispatch({ + type: "MESSAGE_UPDATE", + message: msg + })) + ); + continue; + } + } + + const messageProps: MessageEmbedProps = { + message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]), + channel: linkedChannel, + guildID + }; + + const type = settings.store.automodEmbeds; + accessories.push( + type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage)) + ? + : + ); + } + + return accessories.length ? <>{accessories} : null; +} + +function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null { + const isDM = guildID === "@me"; + + const guild = !isDM && GuildStore.getGuild(channel.guild_id); + const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]); + + + return + {isDM ? "Direct Message - " : (guild as Guild).name + " - "} + {isDM + ? Parser.parse(`<@${dmReceiver.id}>`) + : Parser.parse(`<#${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={() => ( +
+ +
+ )} + />; +} + +const compactModeEnabled = getSettingStoreLazy("textAndImages", "messageDisplayCompact")!; + +function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { + const { message, channel, guildID } = props; + const isDM = guildID === "@me"; + const images = getImages(message); + const { parse } = Parser; + + return + {isDM + ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) + : parse(`<#${channel.id}>`) + } + {isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name} + + } + compact={compactModeEnabled.getSetting()} + content={ + <> + {message.content || message.attachments.length <= images.length + ? parse(message.content) + : [noContent(message.attachments.length, message.embeds.length)] + } + {images.map(a => { + const { width, height } = computeWidthAndHeight(a.width, a.height); + return ( +
+ +
+ ); + })} + + } + hideTimestamp={false} + message={message} + _messageEmbed="automod" + />; +} + +export default definePlugin({ + name: "MessageLinkEmbeds", + description: "Adds a preview to messages that link another message", + authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev], + dependencies: ["MessageAccessoriesAPI", "SettingsStoreAPI"], + patches: [ + { + find: ".embedCard", + replacement: [{ + match: /function (\i)\(\i\){var \i=\i\.message,\i=\i\.channel.{0,200}\.hideTimestamp/, + replace: "$self.AutoModEmbed=$1;$&" + }] + } + ], + + set AutoModEmbed(e: any) { + AutoModEmbed = e; + }, + + settings, + + start() { + addAccessory("messageLinkEmbed", props => { + if (!messageLinkRegex.test(props.message.content)) + return null; + + // need to reset the regex because it's global + messageLinkRegex.lastIndex = 0; + + return ( + + + + ); + }, 4 /* just above rich embeds */); + }, +}); diff --git a/src/plugins/messageTags.ts b/src/plugins/messageTags.ts deleted file mode 100644 index d65c388..0000000 --- a/src/plugins/messageTags.ts +++ /dev/null @@ -1,249 +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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands"; -import * as DataStore from "@api/DataStore"; -import { Settings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const EMOTE = "<:luna:1035316192220553236>"; -const DATA_KEY = "MessageTags_TAGS"; -const MessageTagsMarker = Symbol("MessageTags"); -const author = { - id: "821472922140803112", - bot: false -}; - -interface Tag { - name: string; - message: string; - enabled: boolean; -} - -const getTags = () => DataStore.get(DATA_KEY).then(t => t ?? []); -const getTag = (name: string) => DataStore.get(DATA_KEY).then((t: Tag[]) => (t ?? []).find((tt: Tag) => tt.name === name) ?? null); -const addTag = async (tag: Tag) => { - const tags = await getTags(); - tags.push(tag); - DataStore.set(DATA_KEY, tags); - return tags; -}; -const removeTag = async (name: string) => { - let tags = await getTags(); - tags = await tags.filter((t: Tag) => t.name !== name); - DataStore.set(DATA_KEY, tags); - return tags; -}; - -function createTagCommand(tag: Tag) { - registerCommand({ - name: tag.name, - description: tag.name, - inputType: ApplicationCommandInputType.BUILT_IN_TEXT, - execute: async (_, ctx) => { - if (!await getTag(tag.name)) { - sendBotMessage(ctx.channel.id, { - author, - content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)` - }); - return { content: `/${tag.name}` }; - } - - if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, { - author, - content: `${EMOTE} The tag **${tag.name}** has been sent!` - }); - return { content: tag.message.replaceAll("\\n", "\n") }; - }, - [MessageTagsMarker]: true, - }, "CustomTags"); -} - - -export default definePlugin({ - name: "MessageTags", - description: "Allows you to save messages and to use them with a simple command.", - authors: [Devs.Luna], - options: { - clyde: { - name: "Clyde message on send", - description: "If enabled, clyde will send you an ephemeral message when a tag was used.", - type: OptionType.BOOLEAN, - default: true - } - }, - dependencies: ["CommandsAPI"], - - async start() { - for (const tag of await getTags()) createTagCommand(tag); - }, - - commands: [ - { - name: "tags", - description: "Manage all the tags for yourself", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "create", - description: "Create a new tag", - type: ApplicationCommandOptionType.SUB_COMMAND, - options: [ - { - name: "tag-name", - description: "The name of the tag to trigger the response", - type: ApplicationCommandOptionType.STRING, - required: true - }, - { - name: "message", - description: "The message that you will send when using this tag", - type: ApplicationCommandOptionType.STRING, - required: true - } - ] - }, - { - name: "list", - description: "List all tags from yourself", - type: ApplicationCommandOptionType.SUB_COMMAND, - options: [] - }, - { - name: "delete", - description: "Remove a tag from your yourself", - type: ApplicationCommandOptionType.SUB_COMMAND, - options: [ - { - name: "tag-name", - description: "The name of the tag to trigger the response", - type: ApplicationCommandOptionType.STRING, - required: true - } - ] - }, - { - name: "preview", - description: "Preview a tag without sending it publicly", - type: ApplicationCommandOptionType.SUB_COMMAND, - options: [ - { - name: "tag-name", - description: "The name of the tag to trigger the response", - type: ApplicationCommandOptionType.STRING, - required: true - } - ] - } - ], - - async execute(args, ctx) { - - switch (args[0].name) { - case "create": { - const name: string = findOption(args[0].options, "tag-name", ""); - const message: string = findOption(args[0].options, "message", ""); - - if (await getTag(name)) - return sendBotMessage(ctx.channel.id, { - author, - content: `${EMOTE} A Tag with the name **${name}** already exists!` - }); - - const tag = { - name: name, - enabled: true, - message: message - }; - - createTagCommand(tag); - await addTag(tag); - - sendBotMessage(ctx.channel.id, { - author, - content: `${EMOTE} Successfully created the tag **${name}**!` - }); - break; // end 'create' - } - case "delete": { - const name: string = findOption(args[0].options, "tag-name", ""); - - if (!await getTag(name)) - return sendBotMessage(ctx.channel.id, { - author, - content: `${EMOTE} A Tag with the name **${name}** does not exist!` - }); - - unregisterCommand(name); - await removeTag(name); - - sendBotMessage(ctx.channel.id, { - author, - content: `${EMOTE} Successfully deleted the tag **${name}**!` - }); - break; // end 'delete' - } - case "list": { - sendBotMessage(ctx.channel.id, { - author, - embeds: [ - { - // @ts-ignore - title: "All Tags:", - // @ts-ignore - description: (await getTags()) - .map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`) - .join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`, - // @ts-ignore - color: 0xd77f7f, - type: "rich", - } - ] - }); - break; // end 'list' - } - case "preview": { - const name: string = findOption(args[0].options, "tag-name", ""); - const tag = await getTag(name); - - if (!tag) - return sendBotMessage(ctx.channel.id, { - author, - content: `${EMOTE} A Tag with the name **${name}** does not exist!` - }); - - sendBotMessage(ctx.channel.id, { - author, - content: tag.message.replaceAll("\\n", "\n") - }); - break; // end 'preview' - } - - default: { - sendBotMessage(ctx.channel.id, { - author, - content: "Invalid sub-command" - }); - break; - } - } - } - } - ] -}); diff --git a/src/plugins/messageTags/index.ts b/src/plugins/messageTags/index.ts new file mode 100644 index 0000000..d65c388 --- /dev/null +++ b/src/plugins/messageTags/index.ts @@ -0,0 +1,249 @@ +/* + * 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands"; +import * as DataStore from "@api/DataStore"; +import { Settings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +const EMOTE = "<:luna:1035316192220553236>"; +const DATA_KEY = "MessageTags_TAGS"; +const MessageTagsMarker = Symbol("MessageTags"); +const author = { + id: "821472922140803112", + bot: false +}; + +interface Tag { + name: string; + message: string; + enabled: boolean; +} + +const getTags = () => DataStore.get(DATA_KEY).then(t => t ?? []); +const getTag = (name: string) => DataStore.get(DATA_KEY).then((t: Tag[]) => (t ?? []).find((tt: Tag) => tt.name === name) ?? null); +const addTag = async (tag: Tag) => { + const tags = await getTags(); + tags.push(tag); + DataStore.set(DATA_KEY, tags); + return tags; +}; +const removeTag = async (name: string) => { + let tags = await getTags(); + tags = await tags.filter((t: Tag) => t.name !== name); + DataStore.set(DATA_KEY, tags); + return tags; +}; + +function createTagCommand(tag: Tag) { + registerCommand({ + name: tag.name, + description: tag.name, + inputType: ApplicationCommandInputType.BUILT_IN_TEXT, + execute: async (_, ctx) => { + if (!await getTag(tag.name)) { + sendBotMessage(ctx.channel.id, { + author, + content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)` + }); + return { content: `/${tag.name}` }; + } + + if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, { + author, + content: `${EMOTE} The tag **${tag.name}** has been sent!` + }); + return { content: tag.message.replaceAll("\\n", "\n") }; + }, + [MessageTagsMarker]: true, + }, "CustomTags"); +} + + +export default definePlugin({ + name: "MessageTags", + description: "Allows you to save messages and to use them with a simple command.", + authors: [Devs.Luna], + options: { + clyde: { + name: "Clyde message on send", + description: "If enabled, clyde will send you an ephemeral message when a tag was used.", + type: OptionType.BOOLEAN, + default: true + } + }, + dependencies: ["CommandsAPI"], + + async start() { + for (const tag of await getTags()) createTagCommand(tag); + }, + + commands: [ + { + name: "tags", + description: "Manage all the tags for yourself", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [ + { + name: "create", + description: "Create a new tag", + type: ApplicationCommandOptionType.SUB_COMMAND, + options: [ + { + name: "tag-name", + description: "The name of the tag to trigger the response", + type: ApplicationCommandOptionType.STRING, + required: true + }, + { + name: "message", + description: "The message that you will send when using this tag", + type: ApplicationCommandOptionType.STRING, + required: true + } + ] + }, + { + name: "list", + description: "List all tags from yourself", + type: ApplicationCommandOptionType.SUB_COMMAND, + options: [] + }, + { + name: "delete", + description: "Remove a tag from your yourself", + type: ApplicationCommandOptionType.SUB_COMMAND, + options: [ + { + name: "tag-name", + description: "The name of the tag to trigger the response", + type: ApplicationCommandOptionType.STRING, + required: true + } + ] + }, + { + name: "preview", + description: "Preview a tag without sending it publicly", + type: ApplicationCommandOptionType.SUB_COMMAND, + options: [ + { + name: "tag-name", + description: "The name of the tag to trigger the response", + type: ApplicationCommandOptionType.STRING, + required: true + } + ] + } + ], + + async execute(args, ctx) { + + switch (args[0].name) { + case "create": { + const name: string = findOption(args[0].options, "tag-name", ""); + const message: string = findOption(args[0].options, "message", ""); + + if (await getTag(name)) + return sendBotMessage(ctx.channel.id, { + author, + content: `${EMOTE} A Tag with the name **${name}** already exists!` + }); + + const tag = { + name: name, + enabled: true, + message: message + }; + + createTagCommand(tag); + await addTag(tag); + + sendBotMessage(ctx.channel.id, { + author, + content: `${EMOTE} Successfully created the tag **${name}**!` + }); + break; // end 'create' + } + case "delete": { + const name: string = findOption(args[0].options, "tag-name", ""); + + if (!await getTag(name)) + return sendBotMessage(ctx.channel.id, { + author, + content: `${EMOTE} A Tag with the name **${name}** does not exist!` + }); + + unregisterCommand(name); + await removeTag(name); + + sendBotMessage(ctx.channel.id, { + author, + content: `${EMOTE} Successfully deleted the tag **${name}**!` + }); + break; // end 'delete' + } + case "list": { + sendBotMessage(ctx.channel.id, { + author, + embeds: [ + { + // @ts-ignore + title: "All Tags:", + // @ts-ignore + description: (await getTags()) + .map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`) + .join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`, + // @ts-ignore + color: 0xd77f7f, + type: "rich", + } + ] + }); + break; // end 'list' + } + case "preview": { + const name: string = findOption(args[0].options, "tag-name", ""); + const tag = await getTag(name); + + if (!tag) + return sendBotMessage(ctx.channel.id, { + author, + content: `${EMOTE} A Tag with the name **${name}** does not exist!` + }); + + sendBotMessage(ctx.channel.id, { + author, + content: tag.message.replaceAll("\\n", "\n") + }); + break; // end 'preview' + } + + default: { + sendBotMessage(ctx.channel.id, { + author, + content: "Invalid sub-command" + }); + break; + } + } + } + } + ] +}); diff --git a/src/plugins/moreCommands.ts b/src/plugins/moreCommands.ts deleted file mode 100644 index 61312ac..0000000 --- a/src/plugins/moreCommands.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated, Samu 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 { ApplicationCommandInputType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - - -function mock(input: string): string { - let output = ""; - for (let i = 0; i < input.length; i++) { - output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase(); - } - return output; -} - -export default definePlugin({ - name: "MoreCommands", - description: "echo, lenny, mock", - authors: [Devs.Arjix, Devs.echo, Devs.Samu], - dependencies: ["CommandsAPI"], - commands: [ - { - name: "echo", - description: "Sends a message as Clyde (locally)", - options: [OptionalMessageOption], - inputType: ApplicationCommandInputType.BOT, - execute: (opts, ctx) => { - const content = findOption(opts, "message", ""); - - sendBotMessage(ctx.channel.id, { content }); - }, - }, - { - name: "lenny", - description: "Sends a lenny face", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)" - }), - }, - { - name: "mock", - description: "mOcK PeOpLe", - options: [RequiredMessageOption], - execute: opts => ({ - content: mock(findOption(opts, "message", "")) - }), - }, - ] -}); diff --git a/src/plugins/moreCommands/index.ts b/src/plugins/moreCommands/index.ts new file mode 100644 index 0000000..61312ac --- /dev/null +++ b/src/plugins/moreCommands/index.ts @@ -0,0 +1,66 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated, Samu 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 { ApplicationCommandInputType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + + +function mock(input: string): string { + let output = ""; + for (let i = 0; i < input.length; i++) { + output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase(); + } + return output; +} + +export default definePlugin({ + name: "MoreCommands", + description: "echo, lenny, mock", + authors: [Devs.Arjix, Devs.echo, Devs.Samu], + dependencies: ["CommandsAPI"], + commands: [ + { + name: "echo", + description: "Sends a message as Clyde (locally)", + options: [OptionalMessageOption], + inputType: ApplicationCommandInputType.BOT, + execute: (opts, ctx) => { + const content = findOption(opts, "message", ""); + + sendBotMessage(ctx.channel.id, { content }); + }, + }, + { + name: "lenny", + description: "Sends a lenny face", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)" + }), + }, + { + name: "mock", + description: "mOcK PeOpLe", + options: [RequiredMessageOption], + execute: opts => ({ + content: mock(findOption(opts, "message", "")) + }), + }, + ] +}); diff --git a/src/plugins/moreKaomoji.ts b/src/plugins/moreKaomoji.ts deleted file mode 100644 index 9599108..0000000 --- a/src/plugins/moreKaomoji.ts +++ /dev/null @@ -1,48 +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 { findOption, OptionalMessageOption } from "@api/Commands"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "MoreKaomoji", - description: "Adds more Kaomoji to discord. ヽ(´▽`)/", - authors: [Devs.JacobTm], - dependencies: ["CommandsAPI"], - commands: [ - { name: "dissatisfaction", description: " >﹏<" }, - { name: "smug", description: " ಠ_ಠ" }, - { name: "happy", description: " ヽ(´▽`)/" }, - { name: "crying", description: " ಥ_ಥ" }, - { name: "angry", description: " ヽ(`Д´)ノ" }, - { name: "anger", description: " ヽ(o`皿′o)ノ" }, - { name: "joy", description: " <( ̄︶ ̄)>" }, - { name: "blush", description: "૮ ˶ᵔ ᵕ ᵔ˶ ა" }, - { name: "confused", description: "(•ิ_•ิ)?" }, - { name: "sleeping", description: "(ᴗ_ᴗ)" }, - { name: "laughing", description: "o(≧▽≦)o" }, - - ].map(data => ({ - ...data, - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + data.description - }) - })) -}); diff --git a/src/plugins/moreKaomoji/index.ts b/src/plugins/moreKaomoji/index.ts new file mode 100644 index 0000000..9599108 --- /dev/null +++ b/src/plugins/moreKaomoji/index.ts @@ -0,0 +1,48 @@ +/* + * 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 { findOption, OptionalMessageOption } from "@api/Commands"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "MoreKaomoji", + description: "Adds more Kaomoji to discord. ヽ(´▽`)/", + authors: [Devs.JacobTm], + dependencies: ["CommandsAPI"], + commands: [ + { name: "dissatisfaction", description: " >﹏<" }, + { name: "smug", description: " ಠ_ಠ" }, + { name: "happy", description: " ヽ(´▽`)/" }, + { name: "crying", description: " ಥ_ಥ" }, + { name: "angry", description: " ヽ(`Д´)ノ" }, + { name: "anger", description: " ヽ(o`皿′o)ノ" }, + { name: "joy", description: " <( ̄︶ ̄)>" }, + { name: "blush", description: "૮ ˶ᵔ ᵕ ᵔ˶ ა" }, + { name: "confused", description: "(•ิ_•ิ)?" }, + { name: "sleeping", description: "(ᴗ_ᴗ)" }, + { name: "laughing", description: "o(≧▽≦)o" }, + + ].map(data => ({ + ...data, + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + data.description + }) + })) +}); diff --git a/src/plugins/moreUserTags.tsx b/src/plugins/moreUserTags.tsx deleted file mode 100644 index 595a8ed..0000000 --- a/src/plugins/moreUserTags.tsx +++ /dev/null @@ -1,383 +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 { definePluginSettings } from "@api/Settings"; -import { Flex } from "@components/Flex"; -import { Devs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findLazy } from "@webpack"; -import { Card, ChannelStore, Forms, GuildStore, Switch, TextInput, Tooltip, useState } from "@webpack/common"; -import { RC } from "@webpack/types"; -import { Channel, Message, User } from "discord-types/general"; - -type PermissionName = "CREATE_INSTANT_INVITE" | "KICK_MEMBERS" | "BAN_MEMBERS" | "ADMINISTRATOR" | "MANAGE_CHANNELS" | "MANAGE_GUILD" | "CHANGE_NICKNAME" | "MANAGE_NICKNAMES" | "MANAGE_ROLES" | "MANAGE_WEBHOOKS" | "MANAGE_GUILD_EXPRESSIONS" | "CREATE_GUILD_EXPRESSIONS" | "VIEW_AUDIT_LOG" | "VIEW_CHANNEL" | "VIEW_GUILD_ANALYTICS" | "VIEW_CREATOR_MONETIZATION_ANALYTICS" | "MODERATE_MEMBERS" | "SEND_MESSAGES" | "SEND_TTS_MESSAGES" | "MANAGE_MESSAGES" | "EMBED_LINKS" | "ATTACH_FILES" | "READ_MESSAGE_HISTORY" | "MENTION_EVERYONE" | "USE_EXTERNAL_EMOJIS" | "ADD_REACTIONS" | "USE_APPLICATION_COMMANDS" | "MANAGE_THREADS" | "CREATE_PUBLIC_THREADS" | "CREATE_PRIVATE_THREADS" | "USE_EXTERNAL_STICKERS" | "SEND_MESSAGES_IN_THREADS" | "CONNECT" | "SPEAK" | "MUTE_MEMBERS" | "DEAFEN_MEMBERS" | "MOVE_MEMBERS" | "USE_VAD" | "PRIORITY_SPEAKER" | "STREAM" | "USE_EMBEDDED_ACTIVITIES" | "USE_SOUNDBOARD" | "USE_EXTERNAL_SOUNDS" | "REQUEST_TO_SPEAK" | "MANAGE_EVENTS" | "CREATE_EVENTS"; - -interface Tag { - // name used for identifying, must be alphanumeric + underscores - name: string; - // name shown on the tag itself, can be anything probably; automatically uppercase'd - displayName: string; - description: string; - permissions?: PermissionName[]; - condition?(message: Message | null, user: User, channel: Channel): boolean; -} - -interface TagSetting { - text: string; - showInChat: boolean; - showInNotChat: boolean; -} -interface TagSettings { - WEBHOOK: TagSetting, - OWNER: TagSetting, - ADMINISTRATOR: TagSetting, - MODERATOR_STAFF: TagSetting, - MODERATOR: TagSetting, - VOICE_MODERATOR: TagSetting, - [k: string]: TagSetting; -} - -const CLYDE_ID = "1081004946872352958"; - -// PermissionStore.computePermissions is not the same function and doesn't work here -const PermissionUtil = findByPropsLazy("computePermissions", "canEveryoneRole") as { - computePermissions({ ...args }): bigint; -}; - -const Permissions = findByPropsLazy("SEND_MESSAGES", "VIEW_CREATOR_MONETIZATION_ANALYTICS") as Record; -const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number, className?: string, useRemSizes?: boolean; }> & { Types: Record; }; - -const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot(); - -const tags: Tag[] = [ - { - name: "WEBHOOK", - displayName: "Webhook", - description: "Messages sent by webhooks", - condition: isWebhook - }, { - name: "OWNER", - displayName: "Owner", - description: "Owns the server", - condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id - }, { - name: "ADMINISTRATOR", - displayName: "Admin", - description: "Has the administrator permission", - permissions: ["ADMINISTRATOR"] - }, { - name: "MODERATOR_STAFF", - displayName: "Staff", - description: "Can manage the server, channels or roles", - permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"] - }, { - name: "MODERATOR", - displayName: "Mod", - description: "Can manage messages or kick/ban people", - permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"] - }, { - name: "VOICE_MODERATOR", - displayName: "VC Mod", - description: "Can manage voice chats", - permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"] - } -]; -const defaultSettings = Object.fromEntries( - tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }]) -) as TagSettings; - -function SettingsComponent(props: { setValue(v: any): void; }) { - settings.store.tagSettings ??= defaultSettings; - - const [tagSettings, setTagSettings] = useState(settings.store.tagSettings as TagSettings); - const setValue = (v: TagSettings) => { - setTagSettings(v); - props.setValue(v); - }; - - return ( - - {tags.map(t => ( - - - - {({ onMouseEnter, onMouseLeave }) => ( -
- {t.displayName} Tag -
- )} -
-
- - { - tagSettings[t.name].text = v; - setValue(tagSettings); - }} - className={Margins.bottom16} - /> - - { - tagSettings[t.name].showInChat = v; - setValue(tagSettings); - }} - hideBorder - > - Show in messages - - - { - tagSettings[t.name].showInNotChat = v; - setValue(tagSettings); - }} - hideBorder - > - Show in member list and profiles - -
- ))} -
- ); -} - -const settings = definePluginSettings({ - dontShowForBots: { - description: "Don't show extra tags for bots (excluding webhooks)", - type: OptionType.BOOLEAN - }, - dontShowBotTag: { - description: "Only show extra tags for bots / Hide [BOT] text", - type: OptionType.BOOLEAN - }, - tagSettings: { - type: OptionType.COMPONENT, - component: SettingsComponent, - description: "fill me", - } -}); - -export default definePlugin({ - name: "MoreUserTags", - description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)", - authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN], - settings, - patches: [ - // add tags to the tag list - { - find: '.BOT=0]="BOT"', - replacement: [ - // add tags to the exported tags list (Tag.Types) - { - match: /(\i)\[.\.BOT=0\]="BOT";/, - replace: "$&$1=$self.addTagVariants($1);" - } - ] - }, - { - find: ".DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP;", - replacement: [ - // make the tag show the right text - { - match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(\i\.\i\.Messages)\.BOT_TAG_BOT/, - replace: (_, origSwitch, variant, tags, displayedText, strings) => - `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}` - }, - // show OP tags correctly - { - match: /(\i)=(\i)===\i(?:\.\i)?\.ORIGINAL_POSTER/, - replace: "$1=$self.isOPTag($2)" - }, - // add HTML data attributes (for easier theming) - { - match: /children:\[(?=\i,\(0,\i\.jsx\)\("span",{className:\i\(\)\.botText,children:(\i)}\)\])/, - replace: "'data-tag':$1.toLowerCase(),children:[" - } - ], - }, - // in messages - { - find: ".Types.ORIGINAL_POSTER", - replacement: { - match: /return null==(\i)\?null:\(0,/, - replace: "$1=$self.getTag({...arguments[0],origType:$1,location:'chat'});$&" - } - }, - // in the member list - { - find: ".renderBot=function(){", - replacement: { - match: /\.BOT;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag,type:\i/, - replace: ".BOT;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});return type!==null?$2.botTag,type" - } - }, - // pass channel id down props to be used in profiles - { - find: ".hasAvatarForGuild(null==", - replacement: { - match: /(?=usernameIcon:)/, - replace: "moreTags_channelId:arguments[0].channelId," - } - }, - { - find: 'copyMetaData:"User Tag"', - replacement: { - match: /(?=,botClass:)/, - replace: ",moreTags_channelId:arguments[0].moreTags_channelId" - } - }, - // in profiles - { - find: ",botType:", - replacement: { - match: /,botType:(\i\((\i)\)),/g, - replace: ",botType:$self.getTag({user:$2,channelId:arguments[0].moreTags_channelId,origType:$1,location:'not-chat'})," - } - }, - ], - - start() { - if (settings.store.tagSettings) return; - // @ts-ignore - if (!settings.store.visibility_WEBHOOK) settings.store.tagSettings = defaultSettings; - else { - const newSettings = { ...defaultSettings }; - Object.entries(Vencord.PlainSettings.plugins.MoreUserTags).forEach(([name, value]) => { - const [setting, tag] = name.split("_"); - if (setting === "visibility") { - switch (value) { - case "always": - // its the default - break; - case "chat": - newSettings[tag].showInNotChat = false; - break; - case "not-chat": - newSettings[tag].showInChat = false; - break; - case "never": - newSettings[tag].showInChat = false; - newSettings[tag].showInNotChat = false; - break; - } - } - settings.store.tagSettings = newSettings; - delete Vencord.Settings.plugins.MoreUserTags[name]; - }); - } - }, - - getPermissions(user: User, channel: Channel): string[] { - const guild = GuildStore.getGuild(channel?.guild_id); - if (!guild) return []; - - const permissions = PermissionUtil.computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites }); - return Object.entries(Permissions) - .map(([perm, permInt]) => - permissions & permInt ? perm : "" - ) - .filter(Boolean); - }, - - addTagVariants(tagConstant) { - let i = 100; - tags.forEach(({ name }) => { - tagConstant[name] = ++i; - tagConstant[i] = name; - tagConstant[`${name}-BOT`] = ++i; - tagConstant[i] = `${name}-BOT`; - tagConstant[`${name}-OP`] = ++i; - tagConstant[i] = `${name}-OP`; - }); - return tagConstant; - }, - - isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]), - - getTagText(passedTagName: string, strings: Record) { - if (!passedTagName) return strings.BOT_TAG_BOT; - const [tagName, variant] = passedTagName.split("-"); - const tag = tags.find(({ name }) => tagName === name); - if (!tag) return strings.BOT_TAG_BOT; - if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.BOT_TAG_BOT; - - const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName; - switch (variant) { - case "OP": - return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tagText}`; - case "BOT": - return `${strings.BOT_TAG_BOT} • ${tagText}`; - default: - return tagText; - } - }, - - getTag({ - message, user, channelId, origType, location, channel - }: { - message?: Message, - user: User, - channel?: Channel & { isForumPost(): boolean; }, - channelId?: string; - origType?: number; - location: "chat" | "not-chat"; - }): number | null { - if (location === "chat" && user.id === "1") - return Tag.Types.OFFICIAL; - if (user.id === CLYDE_ID) - return Tag.Types.AI; - - let type = typeof origType === "number" ? origType : null; - - channel ??= ChannelStore.getChannel(channelId!) as any; - if (!channel) return type; - - const settings = this.settings.store; - const perms = this.getPermissions(user, channel); - - for (const tag of tags) { - if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue; - if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue; - - if ( - tag.permissions?.some(perm => perms.includes(perm)) || - (tag.condition?.(message!, user, channel)) - ) { - if (channel.isForumPost() && channel.ownerId === user.id) - type = Tag.Types[`${tag.name}-OP`]; - else if (user.bot && !isWebhook(message!, user) && !settings.dontShowBotTag) - type = Tag.Types[`${tag.name}-BOT`]; - else - type = Tag.Types[tag.name]; - break; - } - } - - return type; - } -}); diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx new file mode 100644 index 0000000..595a8ed --- /dev/null +++ b/src/plugins/moreUserTags/index.tsx @@ -0,0 +1,383 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { Flex } from "@components/Flex"; +import { Devs } from "@utils/constants"; +import { Margins } from "@utils/margins"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy, findLazy } from "@webpack"; +import { Card, ChannelStore, Forms, GuildStore, Switch, TextInput, Tooltip, useState } from "@webpack/common"; +import { RC } from "@webpack/types"; +import { Channel, Message, User } from "discord-types/general"; + +type PermissionName = "CREATE_INSTANT_INVITE" | "KICK_MEMBERS" | "BAN_MEMBERS" | "ADMINISTRATOR" | "MANAGE_CHANNELS" | "MANAGE_GUILD" | "CHANGE_NICKNAME" | "MANAGE_NICKNAMES" | "MANAGE_ROLES" | "MANAGE_WEBHOOKS" | "MANAGE_GUILD_EXPRESSIONS" | "CREATE_GUILD_EXPRESSIONS" | "VIEW_AUDIT_LOG" | "VIEW_CHANNEL" | "VIEW_GUILD_ANALYTICS" | "VIEW_CREATOR_MONETIZATION_ANALYTICS" | "MODERATE_MEMBERS" | "SEND_MESSAGES" | "SEND_TTS_MESSAGES" | "MANAGE_MESSAGES" | "EMBED_LINKS" | "ATTACH_FILES" | "READ_MESSAGE_HISTORY" | "MENTION_EVERYONE" | "USE_EXTERNAL_EMOJIS" | "ADD_REACTIONS" | "USE_APPLICATION_COMMANDS" | "MANAGE_THREADS" | "CREATE_PUBLIC_THREADS" | "CREATE_PRIVATE_THREADS" | "USE_EXTERNAL_STICKERS" | "SEND_MESSAGES_IN_THREADS" | "CONNECT" | "SPEAK" | "MUTE_MEMBERS" | "DEAFEN_MEMBERS" | "MOVE_MEMBERS" | "USE_VAD" | "PRIORITY_SPEAKER" | "STREAM" | "USE_EMBEDDED_ACTIVITIES" | "USE_SOUNDBOARD" | "USE_EXTERNAL_SOUNDS" | "REQUEST_TO_SPEAK" | "MANAGE_EVENTS" | "CREATE_EVENTS"; + +interface Tag { + // name used for identifying, must be alphanumeric + underscores + name: string; + // name shown on the tag itself, can be anything probably; automatically uppercase'd + displayName: string; + description: string; + permissions?: PermissionName[]; + condition?(message: Message | null, user: User, channel: Channel): boolean; +} + +interface TagSetting { + text: string; + showInChat: boolean; + showInNotChat: boolean; +} +interface TagSettings { + WEBHOOK: TagSetting, + OWNER: TagSetting, + ADMINISTRATOR: TagSetting, + MODERATOR_STAFF: TagSetting, + MODERATOR: TagSetting, + VOICE_MODERATOR: TagSetting, + [k: string]: TagSetting; +} + +const CLYDE_ID = "1081004946872352958"; + +// PermissionStore.computePermissions is not the same function and doesn't work here +const PermissionUtil = findByPropsLazy("computePermissions", "canEveryoneRole") as { + computePermissions({ ...args }): bigint; +}; + +const Permissions = findByPropsLazy("SEND_MESSAGES", "VIEW_CREATOR_MONETIZATION_ANALYTICS") as Record; +const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number, className?: string, useRemSizes?: boolean; }> & { Types: Record; }; + +const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot(); + +const tags: Tag[] = [ + { + name: "WEBHOOK", + displayName: "Webhook", + description: "Messages sent by webhooks", + condition: isWebhook + }, { + name: "OWNER", + displayName: "Owner", + description: "Owns the server", + condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id + }, { + name: "ADMINISTRATOR", + displayName: "Admin", + description: "Has the administrator permission", + permissions: ["ADMINISTRATOR"] + }, { + name: "MODERATOR_STAFF", + displayName: "Staff", + description: "Can manage the server, channels or roles", + permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"] + }, { + name: "MODERATOR", + displayName: "Mod", + description: "Can manage messages or kick/ban people", + permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"] + }, { + name: "VOICE_MODERATOR", + displayName: "VC Mod", + description: "Can manage voice chats", + permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"] + } +]; +const defaultSettings = Object.fromEntries( + tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }]) +) as TagSettings; + +function SettingsComponent(props: { setValue(v: any): void; }) { + settings.store.tagSettings ??= defaultSettings; + + const [tagSettings, setTagSettings] = useState(settings.store.tagSettings as TagSettings); + const setValue = (v: TagSettings) => { + setTagSettings(v); + props.setValue(v); + }; + + return ( + + {tags.map(t => ( + + + + {({ onMouseEnter, onMouseLeave }) => ( +
+ {t.displayName} Tag +
+ )} +
+
+ + { + tagSettings[t.name].text = v; + setValue(tagSettings); + }} + className={Margins.bottom16} + /> + + { + tagSettings[t.name].showInChat = v; + setValue(tagSettings); + }} + hideBorder + > + Show in messages + + + { + tagSettings[t.name].showInNotChat = v; + setValue(tagSettings); + }} + hideBorder + > + Show in member list and profiles + +
+ ))} +
+ ); +} + +const settings = definePluginSettings({ + dontShowForBots: { + description: "Don't show extra tags for bots (excluding webhooks)", + type: OptionType.BOOLEAN + }, + dontShowBotTag: { + description: "Only show extra tags for bots / Hide [BOT] text", + type: OptionType.BOOLEAN + }, + tagSettings: { + type: OptionType.COMPONENT, + component: SettingsComponent, + description: "fill me", + } +}); + +export default definePlugin({ + name: "MoreUserTags", + description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)", + authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN], + settings, + patches: [ + // add tags to the tag list + { + find: '.BOT=0]="BOT"', + replacement: [ + // add tags to the exported tags list (Tag.Types) + { + match: /(\i)\[.\.BOT=0\]="BOT";/, + replace: "$&$1=$self.addTagVariants($1);" + } + ] + }, + { + find: ".DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP;", + replacement: [ + // make the tag show the right text + { + match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(\i\.\i\.Messages)\.BOT_TAG_BOT/, + replace: (_, origSwitch, variant, tags, displayedText, strings) => + `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}` + }, + // show OP tags correctly + { + match: /(\i)=(\i)===\i(?:\.\i)?\.ORIGINAL_POSTER/, + replace: "$1=$self.isOPTag($2)" + }, + // add HTML data attributes (for easier theming) + { + match: /children:\[(?=\i,\(0,\i\.jsx\)\("span",{className:\i\(\)\.botText,children:(\i)}\)\])/, + replace: "'data-tag':$1.toLowerCase(),children:[" + } + ], + }, + // in messages + { + find: ".Types.ORIGINAL_POSTER", + replacement: { + match: /return null==(\i)\?null:\(0,/, + replace: "$1=$self.getTag({...arguments[0],origType:$1,location:'chat'});$&" + } + }, + // in the member list + { + find: ".renderBot=function(){", + replacement: { + match: /\.BOT;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag,type:\i/, + replace: ".BOT;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});return type!==null?$2.botTag,type" + } + }, + // pass channel id down props to be used in profiles + { + find: ".hasAvatarForGuild(null==", + replacement: { + match: /(?=usernameIcon:)/, + replace: "moreTags_channelId:arguments[0].channelId," + } + }, + { + find: 'copyMetaData:"User Tag"', + replacement: { + match: /(?=,botClass:)/, + replace: ",moreTags_channelId:arguments[0].moreTags_channelId" + } + }, + // in profiles + { + find: ",botType:", + replacement: { + match: /,botType:(\i\((\i)\)),/g, + replace: ",botType:$self.getTag({user:$2,channelId:arguments[0].moreTags_channelId,origType:$1,location:'not-chat'})," + } + }, + ], + + start() { + if (settings.store.tagSettings) return; + // @ts-ignore + if (!settings.store.visibility_WEBHOOK) settings.store.tagSettings = defaultSettings; + else { + const newSettings = { ...defaultSettings }; + Object.entries(Vencord.PlainSettings.plugins.MoreUserTags).forEach(([name, value]) => { + const [setting, tag] = name.split("_"); + if (setting === "visibility") { + switch (value) { + case "always": + // its the default + break; + case "chat": + newSettings[tag].showInNotChat = false; + break; + case "not-chat": + newSettings[tag].showInChat = false; + break; + case "never": + newSettings[tag].showInChat = false; + newSettings[tag].showInNotChat = false; + break; + } + } + settings.store.tagSettings = newSettings; + delete Vencord.Settings.plugins.MoreUserTags[name]; + }); + } + }, + + getPermissions(user: User, channel: Channel): string[] { + const guild = GuildStore.getGuild(channel?.guild_id); + if (!guild) return []; + + const permissions = PermissionUtil.computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites }); + return Object.entries(Permissions) + .map(([perm, permInt]) => + permissions & permInt ? perm : "" + ) + .filter(Boolean); + }, + + addTagVariants(tagConstant) { + let i = 100; + tags.forEach(({ name }) => { + tagConstant[name] = ++i; + tagConstant[i] = name; + tagConstant[`${name}-BOT`] = ++i; + tagConstant[i] = `${name}-BOT`; + tagConstant[`${name}-OP`] = ++i; + tagConstant[i] = `${name}-OP`; + }); + return tagConstant; + }, + + isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]), + + getTagText(passedTagName: string, strings: Record) { + if (!passedTagName) return strings.BOT_TAG_BOT; + const [tagName, variant] = passedTagName.split("-"); + const tag = tags.find(({ name }) => tagName === name); + if (!tag) return strings.BOT_TAG_BOT; + if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.BOT_TAG_BOT; + + const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName; + switch (variant) { + case "OP": + return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tagText}`; + case "BOT": + return `${strings.BOT_TAG_BOT} • ${tagText}`; + default: + return tagText; + } + }, + + getTag({ + message, user, channelId, origType, location, channel + }: { + message?: Message, + user: User, + channel?: Channel & { isForumPost(): boolean; }, + channelId?: string; + origType?: number; + location: "chat" | "not-chat"; + }): number | null { + if (location === "chat" && user.id === "1") + return Tag.Types.OFFICIAL; + if (user.id === CLYDE_ID) + return Tag.Types.AI; + + let type = typeof origType === "number" ? origType : null; + + channel ??= ChannelStore.getChannel(channelId!) as any; + if (!channel) return type; + + const settings = this.settings.store; + const perms = this.getPermissions(user, channel); + + for (const tag of tags) { + if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue; + if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue; + + if ( + tag.permissions?.some(perm => perms.includes(perm)) || + (tag.condition?.(message!, user, channel)) + ) { + if (channel.isForumPost() && channel.ownerId === user.id) + type = Tag.Types[`${tag.name}-OP`]; + else if (user.bot && !isWebhook(message!, user) && !settings.dontShowBotTag) + type = Tag.Types[`${tag.name}-BOT`]; + else + type = Tag.Types[tag.name]; + break; + } + } + + return type; + } +}); diff --git a/src/plugins/moyai.ts b/src/plugins/moyai.ts deleted file mode 100644 index 649b1fb..0000000 --- a/src/plugins/moyai.ts +++ /dev/null @@ -1,177 +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 { definePluginSettings } from "@api/Settings"; -import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent"; -import { Devs } from "@utils/constants"; -import { sleep } from "@utils/misc"; -import definePlugin, { OptionType } from "@utils/types"; -import { RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; -import { Message, ReactionEmoji } from "discord-types/general"; - -interface IMessageCreate { - type: "MESSAGE_CREATE"; - optimistic: boolean; - isPushNotification: boolean; - channelId: string; - message: Message; -} - -interface IReactionAdd { - type: "MESSAGE_REACTION_ADD"; - optimistic: boolean; - channelId: string; - messageId: string; - messageAuthorId: string; - userId: "195136840355807232"; - emoji: ReactionEmoji; -} - -interface IVoiceChannelEffectSendEvent { - type: string; - emoji?: ReactionEmoji; // Just in case... - channelId: string; - userId: string; - animationType: number; - animationId: number; -} - -const MOYAI = "🗿"; -const MOYAI_URL = - "https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3"; -const MOYAI_URL_HD = - "https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai_hd.wav"; - -const settings = definePluginSettings({ - volume: { - description: "Volume of the 🗿🗿🗿", - type: OptionType.SLIDER, - markers: makeRange(0, 1, 0.1), - default: 0.5, - stickToMarkers: false - }, - quality: { - description: "Quality of the 🗿🗿🗿", - type: OptionType.SELECT, - options: [ - { label: "Normal", value: "Normal", default: true }, - { label: "HD", value: "HD" } - ], - }, - triggerWhenUnfocused: { - description: "Trigger the 🗿 even when the window is unfocused", - type: OptionType.BOOLEAN, - default: true - }, - ignoreBots: { - description: "Ignore bots", - type: OptionType.BOOLEAN, - default: true - }, - ignoreBlocked: { - description: "Ignore blocked users", - type: OptionType.BOOLEAN, - default: true - } -}); - -export default definePlugin({ - name: "Moyai", - authors: [Devs.Megu, Devs.Nuckyz], - description: "🗿🗿🗿🗿🗿🗿🗿🗿", - settings, - - flux: { - async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (settings.store.ignoreBots && message.author?.bot) return; - if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(message.author?.id)) return; - if (!message.content) return; - if (channelId !== SelectedChannelStore.getChannelId()) return; - - const moyaiCount = getMoyaiCount(message.content); - - for (let i = 0; i < moyaiCount; i++) { - boom(); - await sleep(300); - } - }, - - MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, messageAuthorId, emoji }: IReactionAdd) { - if (optimistic || type !== "MESSAGE_REACTION_ADD") return; - if (settings.store.ignoreBots && UserStore.getUser(userId)?.bot) return; - if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(messageAuthorId)) return; - if (channelId !== SelectedChannelStore.getChannelId()) return; - - const name = emoji.name.toLowerCase(); - if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; - - boom(); - }, - - VOICE_CHANNEL_EFFECT_SEND({ emoji }: IVoiceChannelEffectSendEvent) { - if (!emoji?.name) return; - const name = emoji.name.toLowerCase(); - if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; - - boom(); - } - } -}); - -function countOccurrences(sourceString: string, subString: string) { - let i = 0; - let lastIdx = 0; - while ((lastIdx = sourceString.indexOf(subString, lastIdx) + 1) !== 0) - i++; - - return i; -} - -function countMatches(sourceString: string, pattern: RegExp) { - if (!pattern.global) - throw new Error("pattern must be global"); - - let i = 0; - while (pattern.test(sourceString)) - i++; - - return i; -} - -const customMoyaiRe = //gi; - -function getMoyaiCount(message: string) { - const count = countOccurrences(message, MOYAI) - + countMatches(message, customMoyaiRe); - - return Math.min(count, 10); -} - -function boom() { - if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return; - const audioElement = document.createElement("audio"); - - audioElement.src = settings.store.quality === "HD" - ? MOYAI_URL_HD - : MOYAI_URL; - - audioElement.volume = settings.store.volume; - audioElement.play(); -} diff --git a/src/plugins/moyai/index.ts b/src/plugins/moyai/index.ts new file mode 100644 index 0000000..649b1fb --- /dev/null +++ b/src/plugins/moyai/index.ts @@ -0,0 +1,177 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent"; +import { Devs } from "@utils/constants"; +import { sleep } from "@utils/misc"; +import definePlugin, { OptionType } from "@utils/types"; +import { RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; +import { Message, ReactionEmoji } from "discord-types/general"; + +interface IMessageCreate { + type: "MESSAGE_CREATE"; + optimistic: boolean; + isPushNotification: boolean; + channelId: string; + message: Message; +} + +interface IReactionAdd { + type: "MESSAGE_REACTION_ADD"; + optimistic: boolean; + channelId: string; + messageId: string; + messageAuthorId: string; + userId: "195136840355807232"; + emoji: ReactionEmoji; +} + +interface IVoiceChannelEffectSendEvent { + type: string; + emoji?: ReactionEmoji; // Just in case... + channelId: string; + userId: string; + animationType: number; + animationId: number; +} + +const MOYAI = "🗿"; +const MOYAI_URL = + "https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3"; +const MOYAI_URL_HD = + "https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai_hd.wav"; + +const settings = definePluginSettings({ + volume: { + description: "Volume of the 🗿🗿🗿", + type: OptionType.SLIDER, + markers: makeRange(0, 1, 0.1), + default: 0.5, + stickToMarkers: false + }, + quality: { + description: "Quality of the 🗿🗿🗿", + type: OptionType.SELECT, + options: [ + { label: "Normal", value: "Normal", default: true }, + { label: "HD", value: "HD" } + ], + }, + triggerWhenUnfocused: { + description: "Trigger the 🗿 even when the window is unfocused", + type: OptionType.BOOLEAN, + default: true + }, + ignoreBots: { + description: "Ignore bots", + type: OptionType.BOOLEAN, + default: true + }, + ignoreBlocked: { + description: "Ignore blocked users", + type: OptionType.BOOLEAN, + default: true + } +}); + +export default definePlugin({ + name: "Moyai", + authors: [Devs.Megu, Devs.Nuckyz], + description: "🗿🗿🗿🗿🗿🗿🗿🗿", + settings, + + flux: { + async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (settings.store.ignoreBots && message.author?.bot) return; + if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(message.author?.id)) return; + if (!message.content) return; + if (channelId !== SelectedChannelStore.getChannelId()) return; + + const moyaiCount = getMoyaiCount(message.content); + + for (let i = 0; i < moyaiCount; i++) { + boom(); + await sleep(300); + } + }, + + MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, messageAuthorId, emoji }: IReactionAdd) { + if (optimistic || type !== "MESSAGE_REACTION_ADD") return; + if (settings.store.ignoreBots && UserStore.getUser(userId)?.bot) return; + if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(messageAuthorId)) return; + if (channelId !== SelectedChannelStore.getChannelId()) return; + + const name = emoji.name.toLowerCase(); + if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; + + boom(); + }, + + VOICE_CHANNEL_EFFECT_SEND({ emoji }: IVoiceChannelEffectSendEvent) { + if (!emoji?.name) return; + const name = emoji.name.toLowerCase(); + if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; + + boom(); + } + } +}); + +function countOccurrences(sourceString: string, subString: string) { + let i = 0; + let lastIdx = 0; + while ((lastIdx = sourceString.indexOf(subString, lastIdx) + 1) !== 0) + i++; + + return i; +} + +function countMatches(sourceString: string, pattern: RegExp) { + if (!pattern.global) + throw new Error("pattern must be global"); + + let i = 0; + while (pattern.test(sourceString)) + i++; + + return i; +} + +const customMoyaiRe = //gi; + +function getMoyaiCount(message: string) { + const count = countOccurrences(message, MOYAI) + + countMatches(message, customMoyaiRe); + + return Math.min(count, 10); +} + +function boom() { + if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return; + const audioElement = document.createElement("audio"); + + audioElement.src = settings.store.quality === "HD" + ? MOYAI_URL_HD + : MOYAI_URL; + + audioElement.volume = settings.store.volume; + audioElement.play(); +} diff --git a/src/plugins/muteNewGuild.tsx b/src/plugins/muteNewGuild.tsx deleted file mode 100644 index e77f64a..0000000 --- a/src/plugins/muteNewGuild.tsx +++ /dev/null @@ -1,74 +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 { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByProps } from "@webpack"; - -const settings = definePluginSettings({ - guild: { - description: "Mute Guild", - type: OptionType.BOOLEAN, - default: true - }, - everyone: { - description: "Suppress @everyone and @here", - type: OptionType.BOOLEAN, - default: true - }, - role: { - description: "Suppress All Role @mentions", - type: OptionType.BOOLEAN, - default: true - } -}); - -export default definePlugin({ - name: "MuteNewGuild", - description: "Mutes newly joined guilds", - authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince], - patches: [ - { - find: ",acceptInvite:function", - replacement: { - match: /INVITE_ACCEPT_SUCCESS.+?;(\i)=null.+?;/, - replace: (m, guildId) => `${m}$self.handleMute(${guildId});` - } - }, - { - find: "{joinGuild:function", - replacement: { - match: /guildId:(\w+),lurker:(\w+).{0,20}\)}\)\);/, - replace: (m, guildId, lurker) => `${m}if(!${lurker})$self.handleMute(${guildId});` - } - } - ], - settings, - - handleMute(guildId: string | null) { - if (guildId === "@me" || guildId === "null" || guildId == null) return; - findByProps("updateGuildNotificationSettings").updateGuildNotificationSettings(guildId, - { - muted: settings.store.guild, - suppress_everyone: settings.store.everyone, - suppress_roles: settings.store.role - } - ); - } -}); diff --git a/src/plugins/muteNewGuild/index.tsx b/src/plugins/muteNewGuild/index.tsx new file mode 100644 index 0000000..e77f64a --- /dev/null +++ b/src/plugins/muteNewGuild/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByProps } from "@webpack"; + +const settings = definePluginSettings({ + guild: { + description: "Mute Guild", + type: OptionType.BOOLEAN, + default: true + }, + everyone: { + description: "Suppress @everyone and @here", + type: OptionType.BOOLEAN, + default: true + }, + role: { + description: "Suppress All Role @mentions", + type: OptionType.BOOLEAN, + default: true + } +}); + +export default definePlugin({ + name: "MuteNewGuild", + description: "Mutes newly joined guilds", + authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince], + patches: [ + { + find: ",acceptInvite:function", + replacement: { + match: /INVITE_ACCEPT_SUCCESS.+?;(\i)=null.+?;/, + replace: (m, guildId) => `${m}$self.handleMute(${guildId});` + } + }, + { + find: "{joinGuild:function", + replacement: { + match: /guildId:(\w+),lurker:(\w+).{0,20}\)}\)\);/, + replace: (m, guildId, lurker) => `${m}if(!${lurker})$self.handleMute(${guildId});` + } + } + ], + settings, + + handleMute(guildId: string | null) { + if (guildId === "@me" || guildId === "null" || guildId == null) return; + findByProps("updateGuildNotificationSettings").updateGuildNotificationSettings(guildId, + { + muted: settings.store.guild, + suppress_everyone: settings.store.everyone, + suppress_roles: settings.store.role + } + ); + } +}); diff --git a/src/plugins/mutualGroupDMs.tsx b/src/plugins/mutualGroupDMs.tsx deleted file mode 100644 index d445522..0000000 --- a/src/plugins/mutualGroupDMs.tsx +++ /dev/null @@ -1,103 +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 { Devs } from "@utils/constants"; -import { isNonNullish } from "@utils/guards"; -import definePlugin from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { Avatar, ChannelStore, Clickable, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common"; -import { Channel, User } from "discord-types/general"; - -const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel"); -const AvatarUtils = findByPropsLazy("getChannelIconURL"); -const UserUtils = findByPropsLazy("getGlobalName"); - -const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds"); -const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon"); - -function getGroupDMName(channel: Channel) { - return channel.name || - channel.recipients - .map(UserStore.getUser) - .filter(isNonNullish) - .map(c => RelationshipStore.getNickname(c.id) || UserUtils.getName(c)) - .join(", "); -} - -export default definePlugin({ - name: "MutualGroupDMs", - description: "Shows mutual group dms in profiles", - authors: [Devs.amia], - - patches: [ - { - find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded - replacement: [ - { - match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/, - replace: '$1?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),' - }, - { - match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/, - replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);" - } - ] - } - ], - - renderMutualGDMs(user: User, onClose: () => void) { - const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => ( - { - onClose(); - SelectedChannelActionCreators.selectPrivateChannel(c.id); - }} - > - - -
-
{getGroupDMName(c)}
-
{c.recipients.length + 1} Members
-
-
- )); - - return ( - - {entries.length > 0 - ? entries - : ( -
-
-
No group dms in common
-
- ) - } -
- ); - } -}); diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx new file mode 100644 index 0000000..d445522 --- /dev/null +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -0,0 +1,103 @@ +/* + * 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 { isNonNullish } from "@utils/guards"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Avatar, ChannelStore, Clickable, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common"; +import { Channel, User } from "discord-types/general"; + +const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel"); +const AvatarUtils = findByPropsLazy("getChannelIconURL"); +const UserUtils = findByPropsLazy("getGlobalName"); + +const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds"); +const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon"); + +function getGroupDMName(channel: Channel) { + return channel.name || + channel.recipients + .map(UserStore.getUser) + .filter(isNonNullish) + .map(c => RelationshipStore.getNickname(c.id) || UserUtils.getName(c)) + .join(", "); +} + +export default definePlugin({ + name: "MutualGroupDMs", + description: "Shows mutual group dms in profiles", + authors: [Devs.amia], + + patches: [ + { + find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded + replacement: [ + { + match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/, + replace: '$1?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),' + }, + { + match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/, + replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);" + } + ] + } + ], + + renderMutualGDMs(user: User, onClose: () => void) { + const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => ( + { + onClose(); + SelectedChannelActionCreators.selectPrivateChannel(c.id); + }} + > + + +
+
{getGroupDMName(c)}
+
{c.recipients.length + 1} Members
+
+
+ )); + + return ( + + {entries.length > 0 + ? entries + : ( +
+
+
No group dms in common
+
+ ) + } +
+ ); + } +}); diff --git a/src/plugins/noBlockedMessages.ts b/src/plugins/noBlockedMessages.ts deleted file mode 100644 index 54bb2d0..0000000 --- a/src/plugins/noBlockedMessages.ts +++ /dev/null @@ -1,64 +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"; -import { findByPropsLazy } from "@webpack"; - -const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked"); - -export default definePlugin({ - name: "NoBlockedMessages", - description: "Hides all blocked messages from chat completely.", - authors: [Devs.rushii, Devs.Samu], - patches: [ - { - find: 'safety_prompt:"DMSpamExperiment",response:"show_redacted_messages"', - replacement: [ - { - match: /\.collapsedReason;return/, - replace: ".collapsedReason;return null;return;" - } - ] - }, - ...[ - 'displayName="MessageStore"', - 'displayName="ReadStateStore"' - ].map(find => ({ - find, - predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true, - replacement: [ - { - match: /(?<=MESSAGE_CREATE:function\((\i)\){)/, - replace: (_, props) => `if($self.isBlocked(${props}.message))return;` - } - ] - })) - ], - options: { - ignoreBlockedMessages: { - description: "Completely ignores (recent) incoming messages from blocked users (locally).", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: true, - }, - }, - isBlocked: message => - RelationshipStore.isBlocked(message.author.id) -}); diff --git a/src/plugins/noBlockedMessages/index.ts b/src/plugins/noBlockedMessages/index.ts new file mode 100644 index 0000000..54bb2d0 --- /dev/null +++ b/src/plugins/noBlockedMessages/index.ts @@ -0,0 +1,64 @@ +/* + * 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"; +import { findByPropsLazy } from "@webpack"; + +const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked"); + +export default definePlugin({ + name: "NoBlockedMessages", + description: "Hides all blocked messages from chat completely.", + authors: [Devs.rushii, Devs.Samu], + patches: [ + { + find: 'safety_prompt:"DMSpamExperiment",response:"show_redacted_messages"', + replacement: [ + { + match: /\.collapsedReason;return/, + replace: ".collapsedReason;return null;return;" + } + ] + }, + ...[ + 'displayName="MessageStore"', + 'displayName="ReadStateStore"' + ].map(find => ({ + find, + predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true, + replacement: [ + { + match: /(?<=MESSAGE_CREATE:function\((\i)\){)/, + replace: (_, props) => `if($self.isBlocked(${props}.message))return;` + } + ] + })) + ], + options: { + ignoreBlockedMessages: { + description: "Completely ignores (recent) incoming messages from blocked users (locally).", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true, + }, + }, + isBlocked: message => + RelationshipStore.isBlocked(message.author.id) +}); diff --git a/src/plugins/noDevtoolsWarning.ts b/src/plugins/noDevtoolsWarning.ts deleted file mode 100644 index 188b8fa..0000000 --- a/src/plugins/noDevtoolsWarning.ts +++ /dev/null @@ -1,33 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoDevtoolsWarning", - description: "Disables the 'HOLD UP' banner in the console. As a side effect, also prevents Discord from hiding your token, which prevents random logouts.", - authors: [Devs.Ven], - patches: [{ - find: "setDevtoolsCallbacks", - replacement: { - match: /if\(.{0,10}\|\|"0.0.0"!==.{0,2}\.remoteApp\.getVersion\(\)\)/, - replace: "if(false)" - } - }] -}); diff --git a/src/plugins/noDevtoolsWarning/index.ts b/src/plugins/noDevtoolsWarning/index.ts new file mode 100644 index 0000000..188b8fa --- /dev/null +++ b/src/plugins/noDevtoolsWarning/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NoDevtoolsWarning", + description: "Disables the 'HOLD UP' banner in the console. As a side effect, also prevents Discord from hiding your token, which prevents random logouts.", + authors: [Devs.Ven], + patches: [{ + find: "setDevtoolsCallbacks", + replacement: { + match: /if\(.{0,10}\|\|"0.0.0"!==.{0,2}\.remoteApp\.getVersion\(\)\)/, + replace: "if(false)" + } + }] +}); diff --git a/src/plugins/noF1.ts b/src/plugins/noF1.ts deleted file mode 100644 index a40be5a..0000000 --- a/src/plugins/noF1.ts +++ /dev/null @@ -1,35 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoF1", - description: "Disables F1 help bind.", - authors: [Devs.Cyn], - patches: [ - { - find: ',"f1"],comboKeysBindGlobal:', - replacement: { - match: ',"f1"],comboKeysBindGlobal:', - replace: "],comboKeysBindGlobal:", - }, - }, - ], -}); diff --git a/src/plugins/noF1/index.ts b/src/plugins/noF1/index.ts new file mode 100644 index 0000000..a40be5a --- /dev/null +++ b/src/plugins/noF1/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NoF1", + description: "Disables F1 help bind.", + authors: [Devs.Cyn], + patches: [ + { + find: ',"f1"],comboKeysBindGlobal:', + replacement: { + match: ',"f1"],comboKeysBindGlobal:', + replace: "],comboKeysBindGlobal:", + }, + }, + ], +}); diff --git a/src/plugins/noPendingCount.ts b/src/plugins/noPendingCount.ts deleted file mode 100644 index 2ce375e..0000000 --- a/src/plugins/noPendingCount.ts +++ /dev/null @@ -1,96 +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 } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; - -const MessageRequestStore = findByPropsLazy("getMessageRequestsCount"); - -const settings = definePluginSettings({ - hideFriendRequestsCount: { - type: OptionType.BOOLEAN, - description: "Hide incoming friend requests count", - default: true, - restartNeeded: true - }, - hideMessageRequestsCount: { - type: OptionType.BOOLEAN, - description: "Hide message requests count", - default: true, - restartNeeded: true - }, - hidePremiumOffersCount: { - type: OptionType.BOOLEAN, - description: "Hide nitro offers count", - default: true, - restartNeeded: true - } -}); - -export default definePlugin({ - name: "NoPendingCount", - description: "Removes the ping count of incoming friend requests, message requests, and nitro offers.", - authors: [Devs.amia], - - settings: settings, - - // Functions used to determine the top left count indicator can be found in the single module that calls getUnacknowledgedOffers(...) - // or by searching for "showProgressBadge:" - patches: [ - { - find: ".getPendingCount=", - predicate: () => settings.store.hideFriendRequestsCount, - replacement: { - match: /(?<=\.getPendingCount=function\(\)\{)/, - replace: "return 0;" - } - }, - { - find: ".getMessageRequestsCount=", - predicate: () => settings.store.hideMessageRequestsCount, - replacement: { - match: /(?<=\.getMessageRequestsCount=function\(\)\{)/, - replace: "return 0;" - } - }, - // This prevents the Message Requests tab from always hiding due to the previous patch (and is compatible with spam requests) - // In short, only the red badge is hidden. Button visibility behavior isn't changed. - { - find: ".getSpamChannelsCount(),", - predicate: () => settings.store.hideMessageRequestsCount, - replacement: { - match: /(?<=getSpamChannelsCount\(\),\i=)\i\.getMessageRequestsCount\(\)/, - replace: "$self.getRealMessageRequestCount()" - } - }, - { - find: "showProgressBadge:", - predicate: () => settings.store.hidePremiumOffersCount, - replacement: { - match: /\(function\(\){return \i\.\i\.getUnacknowledgedOffers\(\i\)\.length}\)/, - replace: "(function(){return 0})" - } - } - ], - - getRealMessageRequestCount() { - return MessageRequestStore.getMessageRequestChannelIds().size; - } -}); diff --git a/src/plugins/noPendingCount/index.ts b/src/plugins/noPendingCount/index.ts new file mode 100644 index 0000000..2ce375e --- /dev/null +++ b/src/plugins/noPendingCount/index.ts @@ -0,0 +1,96 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; + +const MessageRequestStore = findByPropsLazy("getMessageRequestsCount"); + +const settings = definePluginSettings({ + hideFriendRequestsCount: { + type: OptionType.BOOLEAN, + description: "Hide incoming friend requests count", + default: true, + restartNeeded: true + }, + hideMessageRequestsCount: { + type: OptionType.BOOLEAN, + description: "Hide message requests count", + default: true, + restartNeeded: true + }, + hidePremiumOffersCount: { + type: OptionType.BOOLEAN, + description: "Hide nitro offers count", + default: true, + restartNeeded: true + } +}); + +export default definePlugin({ + name: "NoPendingCount", + description: "Removes the ping count of incoming friend requests, message requests, and nitro offers.", + authors: [Devs.amia], + + settings: settings, + + // Functions used to determine the top left count indicator can be found in the single module that calls getUnacknowledgedOffers(...) + // or by searching for "showProgressBadge:" + patches: [ + { + find: ".getPendingCount=", + predicate: () => settings.store.hideFriendRequestsCount, + replacement: { + match: /(?<=\.getPendingCount=function\(\)\{)/, + replace: "return 0;" + } + }, + { + find: ".getMessageRequestsCount=", + predicate: () => settings.store.hideMessageRequestsCount, + replacement: { + match: /(?<=\.getMessageRequestsCount=function\(\)\{)/, + replace: "return 0;" + } + }, + // This prevents the Message Requests tab from always hiding due to the previous patch (and is compatible with spam requests) + // In short, only the red badge is hidden. Button visibility behavior isn't changed. + { + find: ".getSpamChannelsCount(),", + predicate: () => settings.store.hideMessageRequestsCount, + replacement: { + match: /(?<=getSpamChannelsCount\(\),\i=)\i\.getMessageRequestsCount\(\)/, + replace: "$self.getRealMessageRequestCount()" + } + }, + { + find: "showProgressBadge:", + predicate: () => settings.store.hidePremiumOffersCount, + replacement: { + match: /\(function\(\){return \i\.\i\.getUnacknowledgedOffers\(\i\)\.length}\)/, + replace: "(function(){return 0})" + } + } + ], + + getRealMessageRequestCount() { + return MessageRequestStore.getMessageRequestChannelIds().size; + } +}); diff --git a/src/plugins/noProfileThemes.ts b/src/plugins/noProfileThemes.ts deleted file mode 100644 index 97d195e..0000000 --- a/src/plugins/noProfileThemes.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoProfileThemes", - description: "Completely removes Nitro profile themes", - authors: [Devs.TheKodeToad], - patches: [ - { - find: ".NITRO_BANNER,", - replacement: { - // = isPremiumAtLeast(user.premiumType, TIER_2) - match: /=(?=\i\.\i\.isPremiumAtLeast\(null==(\i))/, - // = user.banner && isPremiumAtLeast(user.premiumType, TIER_2) - replace: "=$1?.banner&&" - } - }, - { - find: "().avatarPositionPremiumNoBanner,default:", - replacement: { - // premiumUserWithoutBanner: foo().avatarPositionPremiumNoBanner, default: foo().avatarPositionNormal - match: /\.avatarPositionPremiumNoBanner(?=,default:\i\(\)\.(\i))/, - // premiumUserWithoutBanner: foo().avatarPositionNormal... - replace: ".$1" - } - }, - { - find: ".hasThemeColors=function(){", - replacement: { - match: /(?<=key:"canUsePremiumProfileCustomization",get:function\(\){return)/, - replace: " false;" - } - } - ] -}); diff --git a/src/plugins/noProfileThemes/index.ts b/src/plugins/noProfileThemes/index.ts new file mode 100644 index 0000000..97d195e --- /dev/null +++ b/src/plugins/noProfileThemes/index.ts @@ -0,0 +1,53 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NoProfileThemes", + description: "Completely removes Nitro profile themes", + authors: [Devs.TheKodeToad], + patches: [ + { + find: ".NITRO_BANNER,", + replacement: { + // = isPremiumAtLeast(user.premiumType, TIER_2) + match: /=(?=\i\.\i\.isPremiumAtLeast\(null==(\i))/, + // = user.banner && isPremiumAtLeast(user.premiumType, TIER_2) + replace: "=$1?.banner&&" + } + }, + { + find: "().avatarPositionPremiumNoBanner,default:", + replacement: { + // premiumUserWithoutBanner: foo().avatarPositionPremiumNoBanner, default: foo().avatarPositionNormal + match: /\.avatarPositionPremiumNoBanner(?=,default:\i\(\)\.(\i))/, + // premiumUserWithoutBanner: foo().avatarPositionNormal... + replace: ".$1" + } + }, + { + find: ".hasThemeColors=function(){", + replacement: { + match: /(?<=key:"canUsePremiumProfileCustomization",get:function\(\){return)/, + replace: " false;" + } + } + ] +}); diff --git a/src/plugins/noRPC.discordDesktop.ts b/src/plugins/noRPC.discordDesktop.ts deleted file mode 100644 index ebbbd5e..0000000 --- a/src/plugins/noRPC.discordDesktop.ts +++ /dev/null @@ -1,35 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoRPC", - description: "Disables Discord's RPC server.", - authors: [Devs.Cyn], - patches: [ - { - find: '.ensureModule("discord_rpc")', - replacement: { - match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)\)}/, - replace: '.ensureModule("discord_rpc")}', - }, - }, - ], -}); diff --git a/src/plugins/noRPC.discordDesktop/index.ts b/src/plugins/noRPC.discordDesktop/index.ts new file mode 100644 index 0000000..ebbbd5e --- /dev/null +++ b/src/plugins/noRPC.discordDesktop/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NoRPC", + description: "Disables Discord's RPC server.", + authors: [Devs.Cyn], + patches: [ + { + find: '.ensureModule("discord_rpc")', + replacement: { + match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)\)}/, + replace: '.ensureModule("discord_rpc")}', + }, + }, + ], +}); diff --git a/src/plugins/noReplyMention.tsx b/src/plugins/noReplyMention.tsx deleted file mode 100644 index 16b3a3e..0000000 --- a/src/plugins/noReplyMention.tsx +++ /dev/null @@ -1,74 +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 { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import type { Message } from "discord-types/general"; - -const settings = definePluginSettings({ - userList: { - description: - "List of users to allow or exempt pings for (separated by commas or spaces)", - type: OptionType.STRING, - default: "1234567890123445,1234567890123445", - }, - shouldPingListed: { - description: "Behaviour", - type: OptionType.SELECT, - options: [ - { - label: "Do not ping the listed users", - value: false, - }, - { - label: "Only ping the listed users", - value: true, - default: true, - }, - ], - }, - inverseShiftReply: { - description: "Invert Discord's shift replying behaviour (enable to make shift reply mention user)", - type: OptionType.BOOLEAN, - default: false, - } -}); - -export default definePlugin({ - name: "NoReplyMention", - description: "Disables reply pings by default", - authors: [Devs.DustyAngel47, Devs.axyie, Devs.pylix, Devs.outfoxxed], - settings, - - shouldMention(message: Message, isHoldingShift: boolean) { - const isListed = settings.store.userList.includes(message.author.id); - const isExempt = settings.store.shouldPingListed ? isListed : !isListed; - return settings.store.inverseShiftReply ? isHoldingShift !== isExempt : !isHoldingShift && isExempt; - }, - - patches: [ - { - find: ",\"Message\")}function", - replacement: { - match: /:(\i),shouldMention:!(\i)\.shiftKey/, - replace: ":$1,shouldMention:$self.shouldMention($1,$2.shiftKey)" - } - } - ], -}); diff --git a/src/plugins/noReplyMention/index.tsx b/src/plugins/noReplyMention/index.tsx new file mode 100644 index 0000000..16b3a3e --- /dev/null +++ b/src/plugins/noReplyMention/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import type { Message } from "discord-types/general"; + +const settings = definePluginSettings({ + userList: { + description: + "List of users to allow or exempt pings for (separated by commas or spaces)", + type: OptionType.STRING, + default: "1234567890123445,1234567890123445", + }, + shouldPingListed: { + description: "Behaviour", + type: OptionType.SELECT, + options: [ + { + label: "Do not ping the listed users", + value: false, + }, + { + label: "Only ping the listed users", + value: true, + default: true, + }, + ], + }, + inverseShiftReply: { + description: "Invert Discord's shift replying behaviour (enable to make shift reply mention user)", + type: OptionType.BOOLEAN, + default: false, + } +}); + +export default definePlugin({ + name: "NoReplyMention", + description: "Disables reply pings by default", + authors: [Devs.DustyAngel47, Devs.axyie, Devs.pylix, Devs.outfoxxed], + settings, + + shouldMention(message: Message, isHoldingShift: boolean) { + const isListed = settings.store.userList.includes(message.author.id); + const isExempt = settings.store.shouldPingListed ? isListed : !isListed; + return settings.store.inverseShiftReply ? isHoldingShift !== isExempt : !isHoldingShift && isExempt; + }, + + patches: [ + { + find: ",\"Message\")}function", + replacement: { + match: /:(\i),shouldMention:!(\i)\.shiftKey/, + replace: ":$1,shouldMention:$self.shouldMention($1,$2.shiftKey)" + } + } + ], +}); diff --git a/src/plugins/noScreensharePreview.ts b/src/plugins/noScreensharePreview.ts deleted file mode 100644 index df3daad..0000000 --- a/src/plugins/noScreensharePreview.ts +++ /dev/null @@ -1,38 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoScreensharePreview", - description: "Disables screenshare previews from being sent.", - authors: [Devs.Nuckyz], - patches: [ - { - find: '("ApplicationStreamPreviewUploadManager")', - replacement: [ - String.raw`\i\.\i\.makeChunkedRequest\(`, - String.raw`\i\.\i\.post\({url:` - ].map(match => ({ - match: new RegExp(String.raw`(?=return\[(\d),${match}\i\.\i\.STREAM_PREVIEW.+?}\)\];)`), - replace: (_, code) => `return[${code},Promise.resolve({body:"",status:204})];` - })) - } - ] -}); diff --git a/src/plugins/noScreensharePreview/index.ts b/src/plugins/noScreensharePreview/index.ts new file mode 100644 index 0000000..df3daad --- /dev/null +++ b/src/plugins/noScreensharePreview/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NoScreensharePreview", + description: "Disables screenshare previews from being sent.", + authors: [Devs.Nuckyz], + patches: [ + { + find: '("ApplicationStreamPreviewUploadManager")', + replacement: [ + String.raw`\i\.\i\.makeChunkedRequest\(`, + String.raw`\i\.\i\.post\({url:` + ].map(match => ({ + match: new RegExp(String.raw`(?=return\[(\d),${match}\i\.\i\.STREAM_PREVIEW.+?}\)\];)`), + replace: (_, code) => `return[${code},Promise.resolve({body:"",status:204})];` + })) + } + ] +}); diff --git a/src/plugins/noSystemBadge.discordDesktop.ts b/src/plugins/noSystemBadge.discordDesktop.ts deleted file mode 100644 index 591a0be..0000000 --- a/src/plugins/noSystemBadge.discordDesktop.ts +++ /dev/null @@ -1,41 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoSystemBadge", - description: "Disables the taskbar and system tray unread count badge.", - authors: [Devs.rushii], - patches: [ - { - find: "setSystemTrayApplications:function", - replacement: [ - { - match: /setBadge:function.+?},/, - replace: "setBadge:function(){}," - }, - { - match: /setSystemTrayIcon:function.+?},/, - replace: "setSystemTrayIcon:function(){}," - } - ] - } - ] -}); diff --git a/src/plugins/noSystemBadge.discordDesktop/index.ts b/src/plugins/noSystemBadge.discordDesktop/index.ts new file mode 100644 index 0000000..591a0be --- /dev/null +++ b/src/plugins/noSystemBadge.discordDesktop/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NoSystemBadge", + description: "Disables the taskbar and system tray unread count badge.", + authors: [Devs.rushii], + patches: [ + { + find: "setSystemTrayApplications:function", + replacement: [ + { + match: /setBadge:function.+?},/, + replace: "setBadge:function(){}," + }, + { + match: /setSystemTrayIcon:function.+?},/, + replace: "setSystemTrayIcon:function(){}," + } + ] + } + ] +}); diff --git a/src/plugins/noUnblockToJump.ts b/src/plugins/noUnblockToJump.ts deleted file mode 100644 index 15f602b..0000000 --- a/src/plugins/noUnblockToJump.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Sofia Lima - * - * 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: "NoUnblockToJump", - description: "Allows you to jump to messages of blocked users without unblocking them", - authors: [Devs.dzshn], - patches: [ - { - find: '.id,"Search Results"', - replacement: { - match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}UNBLOCK_TO_JUMP_TITLE)/, - replace: "if(false)$1" - } - }, - { - find: "renderJumpButton=function()", - replacement: { - match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}UNBLOCK_TO_JUMP_TITLE)/, - replace: "if(false)$1" - } - }, - { - find: "flash:!0,returnMessageId", - replacement: { - match: /.\?(.{1,10}\.show\({.{1,50}UNBLOCK_TO_JUMP_TITLE)/, - replace: "false?$1" - } - } - ] -}); diff --git a/src/plugins/noUnblockToJump/index.ts b/src/plugins/noUnblockToJump/index.ts new file mode 100644 index 0000000..15f602b --- /dev/null +++ b/src/plugins/noUnblockToJump/index.ts @@ -0,0 +1,50 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Sofia Lima + * + * 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: "NoUnblockToJump", + description: "Allows you to jump to messages of blocked users without unblocking them", + authors: [Devs.dzshn], + patches: [ + { + find: '.id,"Search Results"', + replacement: { + match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}UNBLOCK_TO_JUMP_TITLE)/, + replace: "if(false)$1" + } + }, + { + find: "renderJumpButton=function()", + replacement: { + match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}UNBLOCK_TO_JUMP_TITLE)/, + replace: "if(false)$1" + } + }, + { + find: "flash:!0,returnMessageId", + replacement: { + match: /.\?(.{1,10}\.show\({.{1,50}UNBLOCK_TO_JUMP_TITLE)/, + replace: "false?$1" + } + } + ] +}); diff --git a/src/plugins/nsfwGateBypass.ts b/src/plugins/nsfwGateBypass.ts deleted file mode 100644 index 3c5dbb4..0000000 --- a/src/plugins/nsfwGateBypass.ts +++ /dev/null @@ -1,35 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NSFWGateBypass", - description: "Allows you to access NSFW channels without setting/verifying your age", - authors: [Devs.Commandtechno], - patches: [ - { - find: ".nsfwAllowed=null", - replacement: { - match: /(\w+)\.nsfwAllowed=/, - replace: "$1.nsfwAllowed=true;", - }, - }, - ], -}); diff --git a/src/plugins/nsfwGateBypass/index.ts b/src/plugins/nsfwGateBypass/index.ts new file mode 100644 index 0000000..3c5dbb4 --- /dev/null +++ b/src/plugins/nsfwGateBypass/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NSFWGateBypass", + description: "Allows you to access NSFW channels without setting/verifying your age", + authors: [Devs.Commandtechno], + patches: [ + { + find: ".nsfwAllowed=null", + replacement: { + match: /(\w+)\.nsfwAllowed=/, + replace: "$1.nsfwAllowed=true;", + }, + }, + ], +}); diff --git a/src/plugins/oneko.ts b/src/plugins/oneko.ts deleted file mode 100644 index d95ba2b..0000000 --- a/src/plugins/oneko.ts +++ /dev/null @@ -1,40 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "oneko", - description: "cat follow mouse (real)", - // Listing adryd here because this literally just evals her script - authors: [Devs.Ven, Devs.adryd], - - start() { - fetch("https://raw.githubusercontent.com/adryd325/oneko.js/5977144dce83e4d71af1de005d16e38eebeb7b72/oneko.js") - .then(x => x.text()) - .then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif")) - .then(eval); - }, - - stop() { - clearInterval(window.onekoInterval); - delete window.onekoInterval; - document.getElementById("oneko")?.remove(); - } -}); diff --git a/src/plugins/oneko/index.ts b/src/plugins/oneko/index.ts new file mode 100644 index 0000000..d95ba2b --- /dev/null +++ b/src/plugins/oneko/index.ts @@ -0,0 +1,40 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "oneko", + description: "cat follow mouse (real)", + // Listing adryd here because this literally just evals her script + authors: [Devs.Ven, Devs.adryd], + + start() { + fetch("https://raw.githubusercontent.com/adryd325/oneko.js/5977144dce83e4d71af1de005d16e38eebeb7b72/oneko.js") + .then(x => x.text()) + .then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif")) + .then(eval); + }, + + stop() { + clearInterval(window.onekoInterval); + delete window.onekoInterval; + document.getElementById("oneko")?.remove(); + } +}); diff --git a/src/plugins/openInApp.ts b/src/plugins/openInApp.ts deleted file mode 100644 index 9fd335b..0000000 --- a/src/plugins/openInApp.ts +++ /dev/null @@ -1,147 +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 } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { showToast, Toasts } from "@webpack/common"; -import type { MouseEvent } from "react"; - -const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/; -const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user)\/(.+)(?:\?.+?)?$/; -const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/; -const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/; - -const settings = definePluginSettings({ - spotify: { - type: OptionType.BOOLEAN, - description: "Open Spotify links in the Spotify app", - default: true, - }, - steam: { - type: OptionType.BOOLEAN, - description: "Open Steam links in the Steam app", - default: true, - }, - epic: { - type: OptionType.BOOLEAN, - description: "Open Epic Games links in the Epic Games Launcher", - default: true, - } -}); - -export default definePlugin({ - name: "OpenInApp", - description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser", - authors: [Devs.Ven], - settings, - - patches: [ - { - find: '"MaskedLinkStore"', - replacement: { - match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,250}\.trusted)/, - replace: "return $self.handleLink(...arguments).then(handled => handled||$1)" - } - }, - // Make Spotify profile activity links open in app on web - { - find: "WEB_OPEN(", - predicate: () => !IS_DISCORD_DESKTOP && settings.store.spotify, - replacement: { - match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g, - replace: "true$1VencordNative.native.openExternal" - } - }, - { - find: ".CONNECTED_ACCOUNT_VIEWED,", - replacement: { - match: /(?<=href:\i,onClick:function\(\i\)\{)(?=\i=(\i)\.type,.{0,50}CONNECTED_ACCOUNT_VIEWED)/, - replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);" - } - } - ], - - async handleLink(data: { href: string; }, event?: MouseEvent) { - if (!data) return false; - - let url = data.href; - if (!IS_WEB && ShortUrlMatcher.test(url)) { - event?.preventDefault(); - // CORS jumpscare - url = await VencordNative.pluginHelpers.OpenInApp.resolveRedirect(url); - } - - spotify: { - if (!settings.store.spotify) break spotify; - - const match = SpotifyMatcher.exec(url); - if (!match) break spotify; - - const [, type, id] = match; - VencordNative.native.openExternal(`spotify:${type}:${id}`); - - event?.preventDefault(); - return true; - } - - steam: { - if (!settings.store.steam) break steam; - - if (!SteamMatcher.test(url)) break steam; - - VencordNative.native.openExternal(`steam://openurl/${url}`); - event?.preventDefault(); - - // Steam does not focus itself so show a toast so it's slightly less confusing - showToast("Opened link in Steam", Toasts.Type.SUCCESS); - return true; - } - - epic: { - if (!settings.store.epic) break epic; - - const match = EpicMatcher.exec(url); - if (!match) break epic; - - VencordNative.native.openExternal(`com.epicgames.launcher://store/${match[1]}`); - event?.preventDefault(); - - return true; - } - - // in case short url didn't end up being something we can handle - if (event?.defaultPrevented) { - window.open(url, "_blank"); - return true; - } - - return false; - }, - - handleAccountView(event: { preventDefault(): void; }, platformType: string, userId: string) { - if (platformType === "spotify" && settings.store.spotify) { - VencordNative.native.openExternal(`spotify:user:${userId}`); - event.preventDefault(); - } else if (platformType === "steam" && settings.store.steam) { - VencordNative.native.openExternal(`steam://openurl/https://steamcommunity.com/profiles/${userId}`); - showToast("Opened link in Steam", Toasts.Type.SUCCESS); - event.preventDefault(); - } - } -}); diff --git a/src/plugins/openInApp/index.ts b/src/plugins/openInApp/index.ts new file mode 100644 index 0000000..9fd335b --- /dev/null +++ b/src/plugins/openInApp/index.ts @@ -0,0 +1,147 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { showToast, Toasts } from "@webpack/common"; +import type { MouseEvent } from "react"; + +const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/; +const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user)\/(.+)(?:\?.+?)?$/; +const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/; +const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/; + +const settings = definePluginSettings({ + spotify: { + type: OptionType.BOOLEAN, + description: "Open Spotify links in the Spotify app", + default: true, + }, + steam: { + type: OptionType.BOOLEAN, + description: "Open Steam links in the Steam app", + default: true, + }, + epic: { + type: OptionType.BOOLEAN, + description: "Open Epic Games links in the Epic Games Launcher", + default: true, + } +}); + +export default definePlugin({ + name: "OpenInApp", + description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser", + authors: [Devs.Ven], + settings, + + patches: [ + { + find: '"MaskedLinkStore"', + replacement: { + match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,250}\.trusted)/, + replace: "return $self.handleLink(...arguments).then(handled => handled||$1)" + } + }, + // Make Spotify profile activity links open in app on web + { + find: "WEB_OPEN(", + predicate: () => !IS_DISCORD_DESKTOP && settings.store.spotify, + replacement: { + match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g, + replace: "true$1VencordNative.native.openExternal" + } + }, + { + find: ".CONNECTED_ACCOUNT_VIEWED,", + replacement: { + match: /(?<=href:\i,onClick:function\(\i\)\{)(?=\i=(\i)\.type,.{0,50}CONNECTED_ACCOUNT_VIEWED)/, + replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);" + } + } + ], + + async handleLink(data: { href: string; }, event?: MouseEvent) { + if (!data) return false; + + let url = data.href; + if (!IS_WEB && ShortUrlMatcher.test(url)) { + event?.preventDefault(); + // CORS jumpscare + url = await VencordNative.pluginHelpers.OpenInApp.resolveRedirect(url); + } + + spotify: { + if (!settings.store.spotify) break spotify; + + const match = SpotifyMatcher.exec(url); + if (!match) break spotify; + + const [, type, id] = match; + VencordNative.native.openExternal(`spotify:${type}:${id}`); + + event?.preventDefault(); + return true; + } + + steam: { + if (!settings.store.steam) break steam; + + if (!SteamMatcher.test(url)) break steam; + + VencordNative.native.openExternal(`steam://openurl/${url}`); + event?.preventDefault(); + + // Steam does not focus itself so show a toast so it's slightly less confusing + showToast("Opened link in Steam", Toasts.Type.SUCCESS); + return true; + } + + epic: { + if (!settings.store.epic) break epic; + + const match = EpicMatcher.exec(url); + if (!match) break epic; + + VencordNative.native.openExternal(`com.epicgames.launcher://store/${match[1]}`); + event?.preventDefault(); + + return true; + } + + // in case short url didn't end up being something we can handle + if (event?.defaultPrevented) { + window.open(url, "_blank"); + return true; + } + + return false; + }, + + handleAccountView(event: { preventDefault(): void; }, platformType: string, userId: string) { + if (platformType === "spotify" && settings.store.spotify) { + VencordNative.native.openExternal(`spotify:user:${userId}`); + event.preventDefault(); + } else if (platformType === "steam" && settings.store.steam) { + VencordNative.native.openExternal(`steam://openurl/https://steamcommunity.com/profiles/${userId}`); + showToast("Opened link in Steam", Toasts.Type.SUCCESS); + event.preventDefault(); + } + } +}); diff --git a/src/plugins/partyMode.ts b/src/plugins/partyMode.ts deleted file mode 100644 index bb822f5..0000000 --- a/src/plugins/partyMode.ts +++ /dev/null @@ -1,105 +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 } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findStoreLazy } from "@webpack"; -import { GenericStore } from "@webpack/common"; - -const PoggerModeSettingsStore: GenericStore = findStoreLazy("PoggermodeSettingsStore"); - -const enum Intensity { - Normal, - Better, - ProjectX, -} - -const settings = definePluginSettings({ - superIntensePartyMode: { - description: "Party intensity", - type: OptionType.SELECT, - options: [ - { label: "Normal", value: Intensity.Normal, default: true }, - { label: "Better", value: Intensity.Better }, - { label: "Project X", value: Intensity.ProjectX }, - ], - restartNeeded: false, - onChange: setSettings - }, -}); - -export default definePlugin({ - name: "Party mode 🎉", - description: "Allows you to use party mode cause the party never ends ✨", - authors: [Devs.UwUDev], - settings, - - start() { - setPoggerState(true); - setSettings(settings.store.superIntensePartyMode); - }, - - stop() { - setPoggerState(false); - }, -}); - -function setPoggerState(state: boolean) { - Object.assign(PoggerModeSettingsStore.__getLocalVars().state, { - enabled: state, - settingsVisible: state - }); -} - -function setSettings(intensity: Intensity) { - const state = { - screenshakeEnabledLocations: { 0: true, 1: true, 2: true }, - shakeIntensity: 1, - confettiSize: 16, - confettiCount: 5, - combosRequiredCount: 1 - }; - - switch (intensity) { - case Intensity.Normal: { - Object.assign(state, { - screenshakeEnabledLocations: { 0: true, 1: false, 2: false }, - combosRequiredCount: 5 - }); - break; - } - case Intensity.Better: { - Object.assign(state, { - confettiSize: 12, - confettiCount: 8, - }); - break; - } - case Intensity.ProjectX: { - Object.assign(state, { - shakeIntensity: 20, - confettiSize: 25, - confettiCount: 15, - }); - break; - } - } - - Object.assign(PoggerModeSettingsStore.__getLocalVars().state, state); -} diff --git a/src/plugins/partyMode/index.ts b/src/plugins/partyMode/index.ts new file mode 100644 index 0000000..bb822f5 --- /dev/null +++ b/src/plugins/partyMode/index.ts @@ -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 { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findStoreLazy } from "@webpack"; +import { GenericStore } from "@webpack/common"; + +const PoggerModeSettingsStore: GenericStore = findStoreLazy("PoggermodeSettingsStore"); + +const enum Intensity { + Normal, + Better, + ProjectX, +} + +const settings = definePluginSettings({ + superIntensePartyMode: { + description: "Party intensity", + type: OptionType.SELECT, + options: [ + { label: "Normal", value: Intensity.Normal, default: true }, + { label: "Better", value: Intensity.Better }, + { label: "Project X", value: Intensity.ProjectX }, + ], + restartNeeded: false, + onChange: setSettings + }, +}); + +export default definePlugin({ + name: "Party mode 🎉", + description: "Allows you to use party mode cause the party never ends ✨", + authors: [Devs.UwUDev], + settings, + + start() { + setPoggerState(true); + setSettings(settings.store.superIntensePartyMode); + }, + + stop() { + setPoggerState(false); + }, +}); + +function setPoggerState(state: boolean) { + Object.assign(PoggerModeSettingsStore.__getLocalVars().state, { + enabled: state, + settingsVisible: state + }); +} + +function setSettings(intensity: Intensity) { + const state = { + screenshakeEnabledLocations: { 0: true, 1: true, 2: true }, + shakeIntensity: 1, + confettiSize: 16, + confettiCount: 5, + combosRequiredCount: 1 + }; + + switch (intensity) { + case Intensity.Normal: { + Object.assign(state, { + screenshakeEnabledLocations: { 0: true, 1: false, 2: false }, + combosRequiredCount: 5 + }); + break; + } + case Intensity.Better: { + Object.assign(state, { + confettiSize: 12, + confettiCount: 8, + }); + break; + } + case Intensity.ProjectX: { + Object.assign(state, { + shakeIntensity: 20, + confettiSize: 25, + confettiCount: 15, + }); + break; + } + } + + Object.assign(PoggerModeSettingsStore.__getLocalVars().state, state); +} diff --git a/src/plugins/petpet.ts b/src/plugins/petpet.ts deleted file mode 100644 index 0bfd21a..0000000 --- a/src/plugins/petpet.ts +++ /dev/null @@ -1,182 +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 { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption, sendBotMessage } from "@api/Commands"; -import { Devs } from "@utils/constants"; -import { makeLazy } from "@utils/lazy"; -import definePlugin from "@utils/types"; -import { findByCodeLazy, findByPropsLazy } from "@webpack"; -import { applyPalette, GIFEncoder, quantize } from "gifenc"; - -const DRAFT_TYPE = 0; -const DEFAULT_DELAY = 20; -const DEFAULT_RESOLUTION = 128; -const FRAMES = 10; - -const getFrames = makeLazy(() => Promise.all( - Array.from( - { length: FRAMES }, - (_, i) => loadImage(`https://raw.githubusercontent.com/VenPlugs/petpet/main/frames/pet${i}.gif`) - )) -); - -const fetchUser = findByCodeLazy(".USER("); -const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); -const UploadStore = findByPropsLazy("getUploads"); - -function loadImage(source: File | string) { - const isFile = source instanceof File; - const url = isFile ? URL.createObjectURL(source) : source; - - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - if (isFile) - URL.revokeObjectURL(url); - resolve(img); - }; - img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event); - img.crossOrigin = "Anonymous"; - img.src = url; - }); -} - -async function resolveImage(options: Argument[], ctx: CommandContext, noServerPfp: boolean): Promise { - for (const opt of options) { - switch (opt.name) { - case "image": - const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0]; - if (upload) { - if (!upload.isImage) throw "Upload is not an image"; - return upload.item.file; - } - break; - case "url": - return opt.value; - case "user": - try { - const user = await fetchUser(opt.value); - return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048"); - } catch (err) { - console.error("[petpet] Failed to fetch user\n", err); - throw "Failed to fetch user. Check the console for more info."; - } - } - } - return null; -} - -export default definePlugin({ - name: "petpet", - description: "Adds a /petpet slash command to create headpet gifs from any image", - authors: [Devs.Ven], - dependencies: ["CommandsAPI"], - commands: [ - { - inputType: ApplicationCommandInputType.BUILT_IN, - name: "petpet", - description: "Create a petpet gif. You can only specify one of the image options", - options: [ - { - name: "delay", - description: "The delay between each frame. Defaults to 20.", - type: ApplicationCommandOptionType.INTEGER - }, - { - name: "resolution", - description: "Resolution for the gif. Defaults to 120. If you enter an insane number and it freezes Discord that's your fault.", - type: ApplicationCommandOptionType.INTEGER - }, - { - name: "image", - description: "Image attachment to use", - type: ApplicationCommandOptionType.ATTACHMENT - }, - { - name: "url", - description: "URL to fetch image from", - type: ApplicationCommandOptionType.STRING - }, - { - name: "user", - description: "User whose avatar to use as image", - type: ApplicationCommandOptionType.USER - }, - { - name: "no-server-pfp", - description: "Use the normal avatar instead of the server specific one when using the 'user' option", - type: ApplicationCommandOptionType.BOOLEAN - } - ], - execute: async (opts, cmdCtx) => { - const frames = await getFrames(); - - const noServerPfp = findOption(opts, "no-server-pfp", false); - try { - var url = await resolveImage(opts, cmdCtx, noServerPfp); - if (!url) throw "No Image specified!"; - } catch (err) { - sendBotMessage(cmdCtx.channel.id, { - content: String(err), - }); - return; - } - - const avatar = await loadImage(url); - - const delay = findOption(opts, "delay", DEFAULT_DELAY); - const resolution = findOption(opts, "resolution", DEFAULT_RESOLUTION); - - const gif = GIFEncoder(); - - const canvas = document.createElement("canvas"); - canvas.width = canvas.height = resolution; - const ctx = canvas.getContext("2d")!; - - for (let i = 0; i < FRAMES; i++) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - const j = i < FRAMES / 2 ? i : FRAMES - i; - const width = 0.8 + j * 0.02; - const height = 0.8 - j * 0.05; - const offsetX = (1 - width) * 0.5 + 0.1; - const offsetY = 1 - height - 0.08; - - ctx.drawImage(avatar, offsetX * resolution, offsetY * resolution, width * resolution, height * resolution); - ctx.drawImage(frames[i], 0, 0, resolution, resolution); - - const { data } = ctx.getImageData(0, 0, resolution, resolution); - const palette = quantize(data, 256); - const index = applyPalette(data, palette); - - gif.writeFrame(index, resolution, resolution, { - transparent: true, - palette, - delay, - }); - } - - gif.finish(); - const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" }); - // Immediately after the command finishes, Discord clears all input, including pending attachments. - // Thus, setTimeout is needed to make this execute after Discord cleared the input - setTimeout(() => promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10); - }, - }, - ] -}); diff --git a/src/plugins/petpet/index.ts b/src/plugins/petpet/index.ts new file mode 100644 index 0000000..0bfd21a --- /dev/null +++ b/src/plugins/petpet/index.ts @@ -0,0 +1,182 @@ +/* + * 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 { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption, sendBotMessage } from "@api/Commands"; +import { Devs } from "@utils/constants"; +import { makeLazy } from "@utils/lazy"; +import definePlugin from "@utils/types"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; +import { applyPalette, GIFEncoder, quantize } from "gifenc"; + +const DRAFT_TYPE = 0; +const DEFAULT_DELAY = 20; +const DEFAULT_RESOLUTION = 128; +const FRAMES = 10; + +const getFrames = makeLazy(() => Promise.all( + Array.from( + { length: FRAMES }, + (_, i) => loadImage(`https://raw.githubusercontent.com/VenPlugs/petpet/main/frames/pet${i}.gif`) + )) +); + +const fetchUser = findByCodeLazy(".USER("); +const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); +const UploadStore = findByPropsLazy("getUploads"); + +function loadImage(source: File | string) { + const isFile = source instanceof File; + const url = isFile ? URL.createObjectURL(source) : source; + + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + if (isFile) + URL.revokeObjectURL(url); + resolve(img); + }; + img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event); + img.crossOrigin = "Anonymous"; + img.src = url; + }); +} + +async function resolveImage(options: Argument[], ctx: CommandContext, noServerPfp: boolean): Promise { + for (const opt of options) { + switch (opt.name) { + case "image": + const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0]; + if (upload) { + if (!upload.isImage) throw "Upload is not an image"; + return upload.item.file; + } + break; + case "url": + return opt.value; + case "user": + try { + const user = await fetchUser(opt.value); + return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048"); + } catch (err) { + console.error("[petpet] Failed to fetch user\n", err); + throw "Failed to fetch user. Check the console for more info."; + } + } + } + return null; +} + +export default definePlugin({ + name: "petpet", + description: "Adds a /petpet slash command to create headpet gifs from any image", + authors: [Devs.Ven], + dependencies: ["CommandsAPI"], + commands: [ + { + inputType: ApplicationCommandInputType.BUILT_IN, + name: "petpet", + description: "Create a petpet gif. You can only specify one of the image options", + options: [ + { + name: "delay", + description: "The delay between each frame. Defaults to 20.", + type: ApplicationCommandOptionType.INTEGER + }, + { + name: "resolution", + description: "Resolution for the gif. Defaults to 120. If you enter an insane number and it freezes Discord that's your fault.", + type: ApplicationCommandOptionType.INTEGER + }, + { + name: "image", + description: "Image attachment to use", + type: ApplicationCommandOptionType.ATTACHMENT + }, + { + name: "url", + description: "URL to fetch image from", + type: ApplicationCommandOptionType.STRING + }, + { + name: "user", + description: "User whose avatar to use as image", + type: ApplicationCommandOptionType.USER + }, + { + name: "no-server-pfp", + description: "Use the normal avatar instead of the server specific one when using the 'user' option", + type: ApplicationCommandOptionType.BOOLEAN + } + ], + execute: async (opts, cmdCtx) => { + const frames = await getFrames(); + + const noServerPfp = findOption(opts, "no-server-pfp", false); + try { + var url = await resolveImage(opts, cmdCtx, noServerPfp); + if (!url) throw "No Image specified!"; + } catch (err) { + sendBotMessage(cmdCtx.channel.id, { + content: String(err), + }); + return; + } + + const avatar = await loadImage(url); + + const delay = findOption(opts, "delay", DEFAULT_DELAY); + const resolution = findOption(opts, "resolution", DEFAULT_RESOLUTION); + + const gif = GIFEncoder(); + + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = resolution; + const ctx = canvas.getContext("2d")!; + + for (let i = 0; i < FRAMES; i++) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const j = i < FRAMES / 2 ? i : FRAMES - i; + const width = 0.8 + j * 0.02; + const height = 0.8 - j * 0.05; + const offsetX = (1 - width) * 0.5 + 0.1; + const offsetY = 1 - height - 0.08; + + ctx.drawImage(avatar, offsetX * resolution, offsetY * resolution, width * resolution, height * resolution); + ctx.drawImage(frames[i], 0, 0, resolution, resolution); + + const { data } = ctx.getImageData(0, 0, resolution, resolution); + const palette = quantize(data, 256); + const index = applyPalette(data, palette); + + gif.writeFrame(index, resolution, resolution, { + transparent: true, + palette, + delay, + }); + } + + gif.finish(); + const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" }); + // Immediately after the command finishes, Discord clears all input, including pending attachments. + // Thus, setTimeout is needed to make this execute after Discord cleared the input + setTimeout(() => promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10); + }, + }, + ] +}); diff --git a/src/plugins/pictureInPicture.tsx b/src/plugins/pictureInPicture.tsx deleted file mode 100644 index d10d42f..0000000 --- a/src/plugins/pictureInPicture.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { React, Tooltip } from "@webpack/common"; - -const settings = definePluginSettings({ - loop: { - description: "Whether to make the PiP video loop or not", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: false - } -}); - -export default definePlugin({ - name: "PictureInPicture", - description: "Adds picture in picture to videos (next to the Download button)", - authors: [Devs.Lumap], - settings, - - patches: [ - { - find: ".onRemoveAttachment,", - replacement: { - match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/, - replace: "$&$1&&$2&&$self.renderPiPButton()," - }, - }, - ], - - renderPiPButton: ErrorBoundary.wrap(() => { - return ( - - {tooltipProps => ( -
{ - const video = e.currentTarget.parentNode!.parentNode!.querySelector("video")!; - const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement; - - videoClone.loop = settings.store.loop; - videoClone.style.display = "none"; - videoClone.onleavepictureinpicture = () => videoClone.remove(); - - function launchPiP() { - videoClone.currentTime = video.currentTime; - videoClone.requestPictureInPicture(); - video.pause(); - videoClone.play(); - } - - if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */) - launchPiP(); - else - videoClone.onloadedmetadata = launchPiP; - }} - > - - - -
- )} -
- ); - }, { noop: true }) -}); diff --git a/src/plugins/pictureInPicture/index.tsx b/src/plugins/pictureInPicture/index.tsx new file mode 100644 index 0000000..d10d42f --- /dev/null +++ b/src/plugins/pictureInPicture/index.tsx @@ -0,0 +1,83 @@ +/* + * 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 ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { React, Tooltip } from "@webpack/common"; + +const settings = definePluginSettings({ + loop: { + description: "Whether to make the PiP video loop or not", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: false + } +}); + +export default definePlugin({ + name: "PictureInPicture", + description: "Adds picture in picture to videos (next to the Download button)", + authors: [Devs.Lumap], + settings, + + patches: [ + { + find: ".onRemoveAttachment,", + replacement: { + match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/, + replace: "$&$1&&$2&&$self.renderPiPButton()," + }, + }, + ], + + renderPiPButton: ErrorBoundary.wrap(() => { + return ( + + {tooltipProps => ( +
{ + const video = e.currentTarget.parentNode!.parentNode!.querySelector("video")!; + const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement; + + videoClone.loop = settings.store.loop; + videoClone.style.display = "none"; + videoClone.onleavepictureinpicture = () => videoClone.remove(); + + function launchPiP() { + videoClone.currentTime = video.currentTime; + videoClone.requestPictureInPicture(); + video.pause(); + videoClone.play(); + } + + if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */) + launchPiP(); + else + videoClone.onloadedmetadata = launchPiP; + }} + > + + + +
+ )} +
+ ); + }, { noop: true }) +}); diff --git a/src/plugins/plainFolderIcon.ts b/src/plugins/plainFolderIcon.ts deleted file mode 100644 index 4c37e1e..0000000 --- a/src/plugins/plainFolderIcon.ts +++ /dev/null @@ -1,33 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "PlainFolderIcon", - description: "Doesn't show the small guild icons in folders", - authors: [Devs.botato], - patches: [{ - find: ".expandedFolderIconWrapper", - replacement: [{ - match: /\(\w\|\|\w\)&&(\(.{0,40}\(.{1,3}\.animated)/, - replace: "$1", - }] - }] -}); diff --git a/src/plugins/plainFolderIcon/index.ts b/src/plugins/plainFolderIcon/index.ts new file mode 100644 index 0000000..4c37e1e --- /dev/null +++ b/src/plugins/plainFolderIcon/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "PlainFolderIcon", + description: "Doesn't show the small guild icons in folders", + authors: [Devs.botato], + patches: [{ + find: ".expandedFolderIconWrapper", + replacement: [{ + match: /\(\w\|\|\w\)&&(\(.{0,40}\(.{1,3}\.animated)/, + replace: "$1", + }] + }] +}); diff --git a/src/plugins/platformIndicators.tsx b/src/plugins/platformIndicators.tsx deleted file mode 100644 index 6a7a8be..0000000 --- a/src/plugins/platformIndicators.tsx +++ /dev/null @@ -1,261 +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 { addBadge, BadgePosition, ProfileBadge, removeBadge } from "@api/Badges"; -import { addDecorator, removeDecorator } from "@api/MemberListDecorators"; -import { addDecoration, removeDecoration } from "@api/MessageDecorations"; -import { Settings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByCodeLazy, findStoreLazy } from "@webpack"; -import { PresenceStore, Tooltip, UserStore } from "@webpack/common"; -import { User } from "discord-types/general"; - -const SessionsStore = findStoreLazy("SessionsStore"); - -function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) { - return ({ color, tooltip }: { color: string; tooltip: string; }) => ( - - {(tooltipProps: any) => ( - - - - )} - - ); -} - -const Icons = { - desktop: Icon("M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z"), - web: Icon("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z"), - mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }), - console: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }), -}; -type Platform = keyof typeof Icons; - -const getStatusColor = findByCodeLazy(".TWITCH", ".STREAMING", ".INVISIBLE"); - -const PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => { - const tooltip = platform[0].toUpperCase() + platform.slice(1); - const Icon = Icons[platform] ?? Icons.desktop; - - return ; -}; - -const getStatus = (id: string): Record => PresenceStore.getState()?.clientStatuses?.[id]; - -const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false }: { user: User; wantMargin?: boolean; wantTopMargin?: boolean; }) => { - if (!user || user.bot) return null; - - if (user.id === UserStore.getCurrentUser().id) { - const sessions = SessionsStore.getSessions(); - if (typeof sessions !== "object") return null; - const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => { - if (a === b) return 0; - if (a === "online") return 1; - if (b === "online") return -1; - if (a === "idle") return 1; - if (b === "idle") return -1; - return 0; - }); - - const ownStatus = Object.values(sortedSessions).reduce((acc: any, curr: any) => { - if (curr.clientInfo.client !== "unknown") - acc[curr.clientInfo.client] = curr.status; - return acc; - }, {}); - - const { clientStatuses } = PresenceStore.getState(); - clientStatuses[UserStore.getCurrentUser().id] = ownStatus; - } - - const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record; - if (!status) return null; - - const icons = Object.entries(status).map(([platform, status]) => ( - - )); - - if (!icons.length) return null; - - return ( - - {icons} - - ); -}; - -const badge: ProfileBadge = { - component: p => , - position: BadgePosition.START, - shouldShow: userInfo => !!Object.keys(getStatus(userInfo.user.id) ?? {}).length, - key: "indicator" -}; - -const indicatorLocations = { - list: { - description: "In the member list", - onEnable: () => addDecorator("platform-indicator", props => - - - - ), - onDisable: () => removeDecorator("platform-indicator") - }, - badges: { - description: "In user profiles, as badges", - onEnable: () => addBadge(badge), - onDisable: () => removeBadge(badge) - }, - messages: { - description: "Inside messages", - onEnable: () => addDecoration("platform-indicator", props => - - - - ), - onDisable: () => removeDecoration("platform-indicator") - } -}; - -export default definePlugin({ - name: "PlatformIndicators", - description: "Adds platform indicators (Desktop, Mobile, Web...) to users", - authors: [Devs.kemo, Devs.TheSun, Devs.Nuckyz, Devs.Ven], - dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"], - - start() { - const settings = Settings.plugins.PlatformIndicators; - const { displayMode } = settings; - - // transfer settings from the old ones, which had a select menu instead of booleans - if (displayMode) { - if (displayMode !== "both") settings[displayMode] = true; - else { - settings.list = true; - settings.badges = true; - } - settings.messages = true; - delete settings.displayMode; - } - - Object.entries(indicatorLocations).forEach(([key, value]) => { - if (settings[key]) value.onEnable(); - }); - }, - - stop() { - Object.entries(indicatorLocations).forEach(([_, value]) => { - value.onDisable(); - }); - }, - - patches: [ - { - find: ".Masks.STATUS_ONLINE_MOBILE", - predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, - replacement: [ - { - // Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status - match: /(?<=return \i\.\i\.Masks\.STATUS_TYPING;)(.+?)(\i)\?(\i\.\i\.Masks\.STATUS_ONLINE_MOBILE):/, - replace: (_, rest, isMobile, mobileMask) => `if(${isMobile})return ${mobileMask};${rest}` - }, - { - // Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status - match: /(switch\(\i\){case \i\.\i\.ONLINE:return )(\i)\?({.+?}):/, - replace: (_, rest, isMobile, component) => `if(${isMobile})return${component};${rest}` - } - ] - }, - { - find: ".AVATAR_STATUS_MOBILE_16;", - predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, - replacement: [ - { - // Return the AVATAR_STATUS_MOBILE size mask if the user is on mobile, no matter the status - match: /\i===\i\.\i\.ONLINE&&(?=.{0,70}\.AVATAR_STATUS_MOBILE_16;)/, - replace: "" - }, - { - // Fix sizes for mobile indicators which aren't online - match: /(?<=\(\i\.status,)(\i)(?=,(\i),\i\))/, - replace: (_, userStatus, isMobile) => `${isMobile}?"online":${userStatus}` - }, - { - // Make isMobile true no matter the status - match: /(?<=\i&&!\i)&&\i===\i\.\i\.ONLINE/, - replace: "" - } - ] - }, - { - find: "isMobileOnline=function", - predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, - replacement: { - // Make isMobileOnline return true no matter what is the user status - match: /(?<=\i\[\i\.\i\.MOBILE\])===\i\.\i\.ONLINE/, - replace: "!= null" - } - } - ], - - options: { - ...Object.fromEntries( - Object.entries(indicatorLocations).map(([key, value]) => { - return [key, { - type: OptionType.BOOLEAN, - description: `Show indicators ${value.description.toLowerCase()}`, - // onChange doesn't give any way to know which setting was changed, so restart required - restartNeeded: true, - default: true - }]; - }) - ), - colorMobileIndicator: { - type: OptionType.BOOLEAN, - description: "Whether to make the mobile indicator match the color of the user status.", - default: true, - restartNeeded: true - } - } -}); diff --git a/src/plugins/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx new file mode 100644 index 0000000..6a7a8be --- /dev/null +++ b/src/plugins/platformIndicators/index.tsx @@ -0,0 +1,261 @@ +/* + * 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 { addBadge, BadgePosition, ProfileBadge, removeBadge } from "@api/Badges"; +import { addDecorator, removeDecorator } from "@api/MemberListDecorators"; +import { addDecoration, removeDecoration } from "@api/MessageDecorations"; +import { Settings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy, findStoreLazy } from "@webpack"; +import { PresenceStore, Tooltip, UserStore } from "@webpack/common"; +import { User } from "discord-types/general"; + +const SessionsStore = findStoreLazy("SessionsStore"); + +function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) { + return ({ color, tooltip }: { color: string; tooltip: string; }) => ( + + {(tooltipProps: any) => ( + + + + )} + + ); +} + +const Icons = { + desktop: Icon("M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z"), + web: Icon("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z"), + mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }), + console: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }), +}; +type Platform = keyof typeof Icons; + +const getStatusColor = findByCodeLazy(".TWITCH", ".STREAMING", ".INVISIBLE"); + +const PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => { + const tooltip = platform[0].toUpperCase() + platform.slice(1); + const Icon = Icons[platform] ?? Icons.desktop; + + return ; +}; + +const getStatus = (id: string): Record => PresenceStore.getState()?.clientStatuses?.[id]; + +const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false }: { user: User; wantMargin?: boolean; wantTopMargin?: boolean; }) => { + if (!user || user.bot) return null; + + if (user.id === UserStore.getCurrentUser().id) { + const sessions = SessionsStore.getSessions(); + if (typeof sessions !== "object") return null; + const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => { + if (a === b) return 0; + if (a === "online") return 1; + if (b === "online") return -1; + if (a === "idle") return 1; + if (b === "idle") return -1; + return 0; + }); + + const ownStatus = Object.values(sortedSessions).reduce((acc: any, curr: any) => { + if (curr.clientInfo.client !== "unknown") + acc[curr.clientInfo.client] = curr.status; + return acc; + }, {}); + + const { clientStatuses } = PresenceStore.getState(); + clientStatuses[UserStore.getCurrentUser().id] = ownStatus; + } + + const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record; + if (!status) return null; + + const icons = Object.entries(status).map(([platform, status]) => ( + + )); + + if (!icons.length) return null; + + return ( + + {icons} + + ); +}; + +const badge: ProfileBadge = { + component: p => , + position: BadgePosition.START, + shouldShow: userInfo => !!Object.keys(getStatus(userInfo.user.id) ?? {}).length, + key: "indicator" +}; + +const indicatorLocations = { + list: { + description: "In the member list", + onEnable: () => addDecorator("platform-indicator", props => + + + + ), + onDisable: () => removeDecorator("platform-indicator") + }, + badges: { + description: "In user profiles, as badges", + onEnable: () => addBadge(badge), + onDisable: () => removeBadge(badge) + }, + messages: { + description: "Inside messages", + onEnable: () => addDecoration("platform-indicator", props => + + + + ), + onDisable: () => removeDecoration("platform-indicator") + } +}; + +export default definePlugin({ + name: "PlatformIndicators", + description: "Adds platform indicators (Desktop, Mobile, Web...) to users", + authors: [Devs.kemo, Devs.TheSun, Devs.Nuckyz, Devs.Ven], + dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"], + + start() { + const settings = Settings.plugins.PlatformIndicators; + const { displayMode } = settings; + + // transfer settings from the old ones, which had a select menu instead of booleans + if (displayMode) { + if (displayMode !== "both") settings[displayMode] = true; + else { + settings.list = true; + settings.badges = true; + } + settings.messages = true; + delete settings.displayMode; + } + + Object.entries(indicatorLocations).forEach(([key, value]) => { + if (settings[key]) value.onEnable(); + }); + }, + + stop() { + Object.entries(indicatorLocations).forEach(([_, value]) => { + value.onDisable(); + }); + }, + + patches: [ + { + find: ".Masks.STATUS_ONLINE_MOBILE", + predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, + replacement: [ + { + // Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status + match: /(?<=return \i\.\i\.Masks\.STATUS_TYPING;)(.+?)(\i)\?(\i\.\i\.Masks\.STATUS_ONLINE_MOBILE):/, + replace: (_, rest, isMobile, mobileMask) => `if(${isMobile})return ${mobileMask};${rest}` + }, + { + // Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status + match: /(switch\(\i\){case \i\.\i\.ONLINE:return )(\i)\?({.+?}):/, + replace: (_, rest, isMobile, component) => `if(${isMobile})return${component};${rest}` + } + ] + }, + { + find: ".AVATAR_STATUS_MOBILE_16;", + predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, + replacement: [ + { + // Return the AVATAR_STATUS_MOBILE size mask if the user is on mobile, no matter the status + match: /\i===\i\.\i\.ONLINE&&(?=.{0,70}\.AVATAR_STATUS_MOBILE_16;)/, + replace: "" + }, + { + // Fix sizes for mobile indicators which aren't online + match: /(?<=\(\i\.status,)(\i)(?=,(\i),\i\))/, + replace: (_, userStatus, isMobile) => `${isMobile}?"online":${userStatus}` + }, + { + // Make isMobile true no matter the status + match: /(?<=\i&&!\i)&&\i===\i\.\i\.ONLINE/, + replace: "" + } + ] + }, + { + find: "isMobileOnline=function", + predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, + replacement: { + // Make isMobileOnline return true no matter what is the user status + match: /(?<=\i\[\i\.\i\.MOBILE\])===\i\.\i\.ONLINE/, + replace: "!= null" + } + } + ], + + options: { + ...Object.fromEntries( + Object.entries(indicatorLocations).map(([key, value]) => { + return [key, { + type: OptionType.BOOLEAN, + description: `Show indicators ${value.description.toLowerCase()}`, + // onChange doesn't give any way to know which setting was changed, so restart required + restartNeeded: true, + default: true + }]; + }) + ), + colorMobileIndicator: { + type: OptionType.BOOLEAN, + description: "Whether to make the mobile indicator match the color of the user status.", + default: true, + restartNeeded: true + } + } +}); diff --git a/src/plugins/quickMention.tsx b/src/plugins/quickMention.tsx deleted file mode 100644 index d0699b9..0000000 --- a/src/plugins/quickMention.tsx +++ /dev/null @@ -1,59 +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 { addButton, removeButton } from "@api/MessagePopover"; -import { Devs } from "@utils/constants"; -import { insertTextIntoChatInputBox } from "@utils/discord"; -import definePlugin from "@utils/types"; -import { ChannelStore } from "@webpack/common"; - -export default definePlugin({ - name: "QuickMention", - authors: [Devs.kemo], - description: "Adds a quick mention button to the message actions bar", - dependencies: ["MessagePopoverAPI"], - - start() { - addButton("QuickMention", msg => { - return { - label: "Quick Mention", - icon: this.Icon, - message: msg, - channel: ChannelStore.getChannel(msg.channel_id), - onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `) - }; - }); - }, - stop() { - removeButton("QuickMention"); - }, - - Icon: () => ( - - - - ), -}); diff --git a/src/plugins/quickMention/index.tsx b/src/plugins/quickMention/index.tsx new file mode 100644 index 0000000..d0699b9 --- /dev/null +++ b/src/plugins/quickMention/index.tsx @@ -0,0 +1,59 @@ +/* + * 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 { addButton, removeButton } from "@api/MessagePopover"; +import { Devs } from "@utils/constants"; +import { insertTextIntoChatInputBox } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { ChannelStore } from "@webpack/common"; + +export default definePlugin({ + name: "QuickMention", + authors: [Devs.kemo], + description: "Adds a quick mention button to the message actions bar", + dependencies: ["MessagePopoverAPI"], + + start() { + addButton("QuickMention", msg => { + return { + label: "Quick Mention", + icon: this.Icon, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `) + }; + }); + }, + stop() { + removeButton("QuickMention"); + }, + + Icon: () => ( + + + + ), +}); diff --git a/src/plugins/quickReply.ts b/src/plugins/quickReply.ts deleted file mode 100644 index 06797bc..0000000 --- a/src/plugins/quickReply.ts +++ /dev/null @@ -1,216 +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 { definePluginSettings, Settings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, SelectedChannelStore, UserStore } from "@webpack/common"; -import { Message } from "discord-types/general"; - -const Kangaroo = findByPropsLazy("jumpToMessage"); - -const isMac = navigator.platform.includes("Mac"); // bruh -let replyIdx = -1; -let editIdx = -1; - - -const enum MentionOptions { - DISABLED, - ENABLED, - NO_REPLY_MENTION_PLUGIN -} - -const settings = definePluginSettings({ - shouldMention: { - type: OptionType.SELECT, - description: "Ping reply by default", - options: [ - { - label: "Follow NoReplyMention", - value: MentionOptions.NO_REPLY_MENTION_PLUGIN, - default: true - }, - { label: "Enabled", value: MentionOptions.ENABLED }, - { label: "Disabled", value: MentionOptions.DISABLED }, - ] - } -}); - -export default definePlugin({ - name: "QuickReply", - authors: [Devs.obscurity, Devs.Ven, Devs.pylix], - description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds", - settings, - - start() { - Dispatcher.subscribe("DELETE_PENDING_REPLY", onDeletePendingReply); - Dispatcher.subscribe("MESSAGE_END_EDIT", onEndEdit); - Dispatcher.subscribe("MESSAGE_START_EDIT", onStartEdit); - Dispatcher.subscribe("CREATE_PENDING_REPLY", onCreatePendingReply); - document.addEventListener("keydown", onKeydown); - }, - - stop() { - Dispatcher.unsubscribe("DELETE_PENDING_REPLY", onDeletePendingReply); - Dispatcher.unsubscribe("MESSAGE_END_EDIT", onEndEdit); - Dispatcher.unsubscribe("MESSAGE_START_EDIT", onStartEdit); - Dispatcher.unsubscribe("CREATE_PENDING_REPLY", onCreatePendingReply); - document.removeEventListener("keydown", onKeydown); - }, -}); - -const onDeletePendingReply = () => replyIdx = -1; -const onEndEdit = () => editIdx = -1; - -function calculateIdx(messages: Message[], id: string) { - const idx = messages.findIndex(m => m.id === id); - return idx === -1 - ? idx - : messages.length - idx - 1; -} - -function onStartEdit({ channelId, messageId, _isQuickEdit }: any) { - if (_isQuickEdit) return; - - const meId = UserStore.getCurrentUser().id; - - const messages = MessageStore.getMessages(channelId)._array.filter(m => m.author.id === meId); - editIdx = calculateIdx(messages, messageId); -} - -function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) { - if (_isQuickReply) return; - - replyIdx = calculateIdx(MessageStore.getMessages(message.channel_id)._array, message.id); -} - -const isCtrl = (e: KeyboardEvent) => isMac ? e.metaKey : e.ctrlKey; -const isAltOrMeta = (e: KeyboardEvent) => e.altKey || (!isMac && e.metaKey); - -function onKeydown(e: KeyboardEvent) { - const isUp = e.key === "ArrowUp"; - if (!isUp && e.key !== "ArrowDown") return; - if (!isCtrl(e) || isAltOrMeta(e)) return; - - if (e.shiftKey) - nextEdit(isUp); - else - nextReply(isUp); -} - -function jumpIfOffScreen(channelId: string, messageId: string) { - const element = document.getElementById("message-content-" + messageId); - if (!element) return; - - const vh = Math.max(document.documentElement.clientHeight, window.innerHeight); - const rect = element.getBoundingClientRect(); - const isOffscreen = rect.bottom < 200 || rect.top - vh >= -200; - - if (isOffscreen) { - Kangaroo.jumpToMessage({ - channelId, - messageId, - flash: false, - jumpType: "INSTANT" - }); - } -} - -function getNextMessage(isUp: boolean, isReply: boolean) { - let messages: Array = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array; - if (!isReply) { // we are editing so only include own - const meId = UserStore.getCurrentUser().id; - messages = messages.filter(m => m.author.id === meId); - } - - const mutate = (i: number) => isUp - ? Math.min(messages.length - 1, i + 1) - : Math.max(-1, i - 1); - - const findNextNonDeleted = (i: number) => { - do { - i = mutate(i); - } while (i !== -1 && messages[messages.length - i - 1]?.deleted === true); - return i; - }; - - let i: number; - if (isReply) - replyIdx = i = findNextNonDeleted(replyIdx); - else - editIdx = i = findNextNonDeleted(editIdx); - - return i === - 1 ? undefined : messages[messages.length - i - 1]; -} - -function shouldMention(message) { - const { enabled, userList, shouldPingListed } = Settings.plugins.NoReplyMention; - const shouldPing = !enabled || (shouldPingListed === userList.includes(message.author.id)); - - switch (settings.store.shouldMention) { - case MentionOptions.NO_REPLY_MENTION_PLUGIN: return shouldPing; - case MentionOptions.DISABLED: return false; - default: return true; - } -} - -// handle next/prev reply -function nextReply(isUp: boolean) { - const message = getNextMessage(isUp, true); - - if (!message) - return void Dispatcher.dispatch({ - type: "DELETE_PENDING_REPLY", - channelId: SelectedChannelStore.getChannelId(), - }); - - const channel = ChannelStore.getChannel(message.channel_id); - const meId = UserStore.getCurrentUser().id; - - Dispatcher.dispatch({ - type: "CREATE_PENDING_REPLY", - channel, - message, - shouldMention: shouldMention(message), - showMentionToggle: channel.guild_id !== null && message.author.id !== meId, - _isQuickReply: true - }); - jumpIfOffScreen(channel.id, message.id); -} - -// handle next/prev edit -function nextEdit(isUp: boolean) { - const message = getNextMessage(isUp, false); - - if (!message) - Dispatcher.dispatch({ - type: "MESSAGE_END_EDIT", - channelId: SelectedChannelStore.getChannelId() - }); - else { - Dispatcher.dispatch({ - type: "MESSAGE_START_EDIT", - channelId: message.channel_id, - messageId: message.id, - content: message.content, - _isQuickEdit: true - }); - jumpIfOffScreen(message.channel_id, message.id); - } -} diff --git a/src/plugins/quickReply/index.ts b/src/plugins/quickReply/index.ts new file mode 100644 index 0000000..06797bc --- /dev/null +++ b/src/plugins/quickReply/index.ts @@ -0,0 +1,216 @@ +/* + * 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 { definePluginSettings, Settings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, SelectedChannelStore, UserStore } from "@webpack/common"; +import { Message } from "discord-types/general"; + +const Kangaroo = findByPropsLazy("jumpToMessage"); + +const isMac = navigator.platform.includes("Mac"); // bruh +let replyIdx = -1; +let editIdx = -1; + + +const enum MentionOptions { + DISABLED, + ENABLED, + NO_REPLY_MENTION_PLUGIN +} + +const settings = definePluginSettings({ + shouldMention: { + type: OptionType.SELECT, + description: "Ping reply by default", + options: [ + { + label: "Follow NoReplyMention", + value: MentionOptions.NO_REPLY_MENTION_PLUGIN, + default: true + }, + { label: "Enabled", value: MentionOptions.ENABLED }, + { label: "Disabled", value: MentionOptions.DISABLED }, + ] + } +}); + +export default definePlugin({ + name: "QuickReply", + authors: [Devs.obscurity, Devs.Ven, Devs.pylix], + description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds", + settings, + + start() { + Dispatcher.subscribe("DELETE_PENDING_REPLY", onDeletePendingReply); + Dispatcher.subscribe("MESSAGE_END_EDIT", onEndEdit); + Dispatcher.subscribe("MESSAGE_START_EDIT", onStartEdit); + Dispatcher.subscribe("CREATE_PENDING_REPLY", onCreatePendingReply); + document.addEventListener("keydown", onKeydown); + }, + + stop() { + Dispatcher.unsubscribe("DELETE_PENDING_REPLY", onDeletePendingReply); + Dispatcher.unsubscribe("MESSAGE_END_EDIT", onEndEdit); + Dispatcher.unsubscribe("MESSAGE_START_EDIT", onStartEdit); + Dispatcher.unsubscribe("CREATE_PENDING_REPLY", onCreatePendingReply); + document.removeEventListener("keydown", onKeydown); + }, +}); + +const onDeletePendingReply = () => replyIdx = -1; +const onEndEdit = () => editIdx = -1; + +function calculateIdx(messages: Message[], id: string) { + const idx = messages.findIndex(m => m.id === id); + return idx === -1 + ? idx + : messages.length - idx - 1; +} + +function onStartEdit({ channelId, messageId, _isQuickEdit }: any) { + if (_isQuickEdit) return; + + const meId = UserStore.getCurrentUser().id; + + const messages = MessageStore.getMessages(channelId)._array.filter(m => m.author.id === meId); + editIdx = calculateIdx(messages, messageId); +} + +function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) { + if (_isQuickReply) return; + + replyIdx = calculateIdx(MessageStore.getMessages(message.channel_id)._array, message.id); +} + +const isCtrl = (e: KeyboardEvent) => isMac ? e.metaKey : e.ctrlKey; +const isAltOrMeta = (e: KeyboardEvent) => e.altKey || (!isMac && e.metaKey); + +function onKeydown(e: KeyboardEvent) { + const isUp = e.key === "ArrowUp"; + if (!isUp && e.key !== "ArrowDown") return; + if (!isCtrl(e) || isAltOrMeta(e)) return; + + if (e.shiftKey) + nextEdit(isUp); + else + nextReply(isUp); +} + +function jumpIfOffScreen(channelId: string, messageId: string) { + const element = document.getElementById("message-content-" + messageId); + if (!element) return; + + const vh = Math.max(document.documentElement.clientHeight, window.innerHeight); + const rect = element.getBoundingClientRect(); + const isOffscreen = rect.bottom < 200 || rect.top - vh >= -200; + + if (isOffscreen) { + Kangaroo.jumpToMessage({ + channelId, + messageId, + flash: false, + jumpType: "INSTANT" + }); + } +} + +function getNextMessage(isUp: boolean, isReply: boolean) { + let messages: Array = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array; + if (!isReply) { // we are editing so only include own + const meId = UserStore.getCurrentUser().id; + messages = messages.filter(m => m.author.id === meId); + } + + const mutate = (i: number) => isUp + ? Math.min(messages.length - 1, i + 1) + : Math.max(-1, i - 1); + + const findNextNonDeleted = (i: number) => { + do { + i = mutate(i); + } while (i !== -1 && messages[messages.length - i - 1]?.deleted === true); + return i; + }; + + let i: number; + if (isReply) + replyIdx = i = findNextNonDeleted(replyIdx); + else + editIdx = i = findNextNonDeleted(editIdx); + + return i === - 1 ? undefined : messages[messages.length - i - 1]; +} + +function shouldMention(message) { + const { enabled, userList, shouldPingListed } = Settings.plugins.NoReplyMention; + const shouldPing = !enabled || (shouldPingListed === userList.includes(message.author.id)); + + switch (settings.store.shouldMention) { + case MentionOptions.NO_REPLY_MENTION_PLUGIN: return shouldPing; + case MentionOptions.DISABLED: return false; + default: return true; + } +} + +// handle next/prev reply +function nextReply(isUp: boolean) { + const message = getNextMessage(isUp, true); + + if (!message) + return void Dispatcher.dispatch({ + type: "DELETE_PENDING_REPLY", + channelId: SelectedChannelStore.getChannelId(), + }); + + const channel = ChannelStore.getChannel(message.channel_id); + const meId = UserStore.getCurrentUser().id; + + Dispatcher.dispatch({ + type: "CREATE_PENDING_REPLY", + channel, + message, + shouldMention: shouldMention(message), + showMentionToggle: channel.guild_id !== null && message.author.id !== meId, + _isQuickReply: true + }); + jumpIfOffScreen(channel.id, message.id); +} + +// handle next/prev edit +function nextEdit(isUp: boolean) { + const message = getNextMessage(isUp, false); + + if (!message) + Dispatcher.dispatch({ + type: "MESSAGE_END_EDIT", + channelId: SelectedChannelStore.getChannelId() + }); + else { + Dispatcher.dispatch({ + type: "MESSAGE_START_EDIT", + channelId: message.channel_id, + messageId: message.id, + content: message.content, + _isQuickEdit: true + }); + jumpIfOffScreen(message.channel_id, message.id); + } +} diff --git a/src/plugins/reactErrorDecoder.ts b/src/plugins/reactErrorDecoder.ts deleted file mode 100644 index 2332d45..0000000 --- a/src/plugins/reactErrorDecoder.ts +++ /dev/null @@ -1,59 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -let ERROR_CODES: any; -const CODES_URL = - "https://raw.githubusercontent.com/facebook/react/17.0.2/scripts/error-codes/codes.json"; - -export default definePlugin({ - name: "ReactErrorDecoder", - description: 'Replaces "Minifed React Error" with the actual error.', - authors: [Devs.Cyn], - patches: [ - { - find: '"https://reactjs.org/docs/error-decoder.html?invariant="', - replacement: { - match: /(function .\(.\)){(for\(var .="https:\/\/reactjs\.org\/docs\/error-decoder\.html\?invariant="\+.,.=1;. - `${func}{var decoded=Vencord.Plugins.plugins.ReactErrorDecoder.decodeError.apply(null, arguments);if(decoded)return decoded;${original}}`, - }, - }, - ], - - async start() { - ERROR_CODES = await fetch(CODES_URL) - .then(res => res.json()) - .catch(e => console.error("[ReactErrorDecoder] Failed to fetch React error codes\n", e)); - }, - - stop() { - ERROR_CODES = undefined; - }, - - decodeError(code: number, ...args: any) { - let index = 0; - return ERROR_CODES?.[code]?.replace(/%s/g, () => { - const arg = args[index]; - index++; - return arg; - }); - }, -}); diff --git a/src/plugins/reactErrorDecoder/index.ts b/src/plugins/reactErrorDecoder/index.ts new file mode 100644 index 0000000..2332d45 --- /dev/null +++ b/src/plugins/reactErrorDecoder/index.ts @@ -0,0 +1,59 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +let ERROR_CODES: any; +const CODES_URL = + "https://raw.githubusercontent.com/facebook/react/17.0.2/scripts/error-codes/codes.json"; + +export default definePlugin({ + name: "ReactErrorDecoder", + description: 'Replaces "Minifed React Error" with the actual error.', + authors: [Devs.Cyn], + patches: [ + { + find: '"https://reactjs.org/docs/error-decoder.html?invariant="', + replacement: { + match: /(function .\(.\)){(for\(var .="https:\/\/reactjs\.org\/docs\/error-decoder\.html\?invariant="\+.,.=1;. + `${func}{var decoded=Vencord.Plugins.plugins.ReactErrorDecoder.decodeError.apply(null, arguments);if(decoded)return decoded;${original}}`, + }, + }, + ], + + async start() { + ERROR_CODES = await fetch(CODES_URL) + .then(res => res.json()) + .catch(e => console.error("[ReactErrorDecoder] Failed to fetch React error codes\n", e)); + }, + + stop() { + ERROR_CODES = undefined; + }, + + decodeError(code: number, ...args: any) { + let index = 0; + return ERROR_CODES?.[code]?.replace(/%s/g, () => { + const arg = args[index]; + index++; + return arg; + }); + }, +}); diff --git a/src/plugins/readAllNotificationsButton.tsx b/src/plugins/readAllNotificationsButton.tsx deleted file mode 100644 index b5b0b5f..0000000 --- a/src/plugins/readAllNotificationsButton.tsx +++ /dev/null @@ -1,71 +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 { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common"; - -function onClick() { - const channels: Array = []; - - Object.values(GuildStore.getGuilds()).forEach(guild => { - GuildChannelStore.getChannels(guild.id).SELECTABLE.forEach((c: { channel: { id: string; }; }) => { - if (!ReadStateStore.hasUnread(c.channel.id)) return; - - channels.push({ - channelId: c.channel.id, - // messageId: c.channel?.lastMessageId, - messageId: ReadStateStore.lastMessageId(c.channel.id), - readStateType: 0 - }); - }); - }); - - FluxDispatcher.dispatch({ - type: "BULK_ACK", - context: "APP", - channels: channels - }); -} - -const ReadAllButton = () => ( - -); - -export default definePlugin({ - name: "ReadAllNotificationsButton", - description: "Read all server notifications with a single button click!", - authors: [Devs.kemo], - dependencies: ["ServerListAPI"], - - renderReadAllButton: () => , - - start() { - addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton); - }, - - stop() { - removeServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton); - } -}); diff --git a/src/plugins/readAllNotificationsButton/index.tsx b/src/plugins/readAllNotificationsButton/index.tsx new file mode 100644 index 0000000..b5b0b5f --- /dev/null +++ b/src/plugins/readAllNotificationsButton/index.tsx @@ -0,0 +1,71 @@ +/* + * 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 { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common"; + +function onClick() { + const channels: Array = []; + + Object.values(GuildStore.getGuilds()).forEach(guild => { + GuildChannelStore.getChannels(guild.id).SELECTABLE.forEach((c: { channel: { id: string; }; }) => { + if (!ReadStateStore.hasUnread(c.channel.id)) return; + + channels.push({ + channelId: c.channel.id, + // messageId: c.channel?.lastMessageId, + messageId: ReadStateStore.lastMessageId(c.channel.id), + readStateType: 0 + }); + }); + }); + + FluxDispatcher.dispatch({ + type: "BULK_ACK", + context: "APP", + channels: channels + }); +} + +const ReadAllButton = () => ( + +); + +export default definePlugin({ + name: "ReadAllNotificationsButton", + description: "Read all server notifications with a single button click!", + authors: [Devs.kemo], + dependencies: ["ServerListAPI"], + + renderReadAllButton: () => , + + start() { + addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton); + }, + + stop() { + removeServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton); + } +}); diff --git a/src/plugins/revealAllSpoilers.ts b/src/plugins/revealAllSpoilers.ts deleted file mode 100644 index a120016..0000000 --- a/src/plugins/revealAllSpoilers.ts +++ /dev/null @@ -1,58 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { findByPropsLazy } from "@webpack"; - -const SpoilerClasses = findByPropsLazy("spoilerContent"); -const MessagesClasses = findByPropsLazy("messagesWrapper", "messages"); - -export default definePlugin({ - name: "RevealAllSpoilers", - description: "Reveal all spoilers in a message by Ctrl-clicking a spoiler, or in the chat with Ctrl+Shift-click", - authors: [Devs.whqwert], - - patches: [ - { - find: ".removeObscurity=function", - replacement: { - match: /(?<=\.removeObscurity=function\((\i)\){)/, - replace: (_, event) => `$self.reveal(${event});` - } - } - ], - - reveal(event: MouseEvent) { - const { ctrlKey, shiftKey, target } = event; - - if (!ctrlKey) { return; } - - const { spoilerContent, hidden } = SpoilerClasses; - const { messagesWrapper } = MessagesClasses; - - const parent = shiftKey - ? document.querySelector(`div.${messagesWrapper}`) - : (target as HTMLSpanElement).parentElement; - - for (const spoiler of parent!.querySelectorAll(`span.${spoilerContent}.${hidden}`)) { - (spoiler as HTMLSpanElement).click(); - } - } - -}); diff --git a/src/plugins/revealAllSpoilers/index.ts b/src/plugins/revealAllSpoilers/index.ts new file mode 100644 index 0000000..a120016 --- /dev/null +++ b/src/plugins/revealAllSpoilers/index.ts @@ -0,0 +1,58 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; + +const SpoilerClasses = findByPropsLazy("spoilerContent"); +const MessagesClasses = findByPropsLazy("messagesWrapper", "messages"); + +export default definePlugin({ + name: "RevealAllSpoilers", + description: "Reveal all spoilers in a message by Ctrl-clicking a spoiler, or in the chat with Ctrl+Shift-click", + authors: [Devs.whqwert], + + patches: [ + { + find: ".removeObscurity=function", + replacement: { + match: /(?<=\.removeObscurity=function\((\i)\){)/, + replace: (_, event) => `$self.reveal(${event});` + } + } + ], + + reveal(event: MouseEvent) { + const { ctrlKey, shiftKey, target } = event; + + if (!ctrlKey) { return; } + + const { spoilerContent, hidden } = SpoilerClasses; + const { messagesWrapper } = MessagesClasses; + + const parent = shiftKey + ? document.querySelector(`div.${messagesWrapper}`) + : (target as HTMLSpanElement).parentElement; + + for (const spoiler of parent!.querySelectorAll(`span.${spoilerContent}.${hidden}`)) { + (spoiler as HTMLSpanElement).click(); + } + } + +}); diff --git a/src/plugins/reverseImageSearch.tsx b/src/plugins/reverseImageSearch.tsx deleted file mode 100644 index 811e7ff..0000000 --- a/src/plugins/reverseImageSearch.tsx +++ /dev/null @@ -1,120 +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 { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; -import { Flex } from "@components/Flex"; -import { OpenExternalIcon } from "@components/Icons"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { Menu } from "@webpack/common"; - -const Engines = { - Google: "https://lens.google.com/uploadbyurl?url=", - Yandex: "https://yandex.com/images/search?rpt=imageview&url=", - SauceNAO: "https://saucenao.com/search.php?url=", - IQDB: "https://iqdb.org/?url=", - TinEye: "https://www.tineye.com/search?url=", - ImgOps: "https://imgops.com/start?url=" -} as const; - -function search(src: string, engine: string) { - open(engine + encodeURIComponent(src), "_blank"); -} - -const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (!props) return; - const { reverseImageSearchType, itemHref, itemSrc } = props; - - if (!reverseImageSearchType || reverseImageSearchType !== "img") return; - - const src = itemHref ?? 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))} - /> - - )); - } -}; - -export default definePlugin({ - name: "ReverseImageSearch", - description: "Adds ImageSearch to image context menus", - authors: [Devs.Ven, Devs.Nuckyz], - tags: ["ImageUtilities"], - - patches: [ - { - find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", - replacement: { - match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/, - replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),` - } - } - ], - - start() { - addContextMenuPatch("message", imageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", imageContextMenuPatch); - } -}); diff --git a/src/plugins/reverseImageSearch/index.tsx b/src/plugins/reverseImageSearch/index.tsx new file mode 100644 index 0000000..811e7ff --- /dev/null +++ b/src/plugins/reverseImageSearch/index.tsx @@ -0,0 +1,120 @@ +/* + * 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 { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { Flex } from "@components/Flex"; +import { OpenExternalIcon } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Menu } from "@webpack/common"; + +const Engines = { + Google: "https://lens.google.com/uploadbyurl?url=", + Yandex: "https://yandex.com/images/search?rpt=imageview&url=", + SauceNAO: "https://saucenao.com/search.php?url=", + IQDB: "https://iqdb.org/?url=", + TinEye: "https://www.tineye.com/search?url=", + ImgOps: "https://imgops.com/start?url=" +} as const; + +function search(src: string, engine: string) { + open(engine + encodeURIComponent(src), "_blank"); +} + +const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (!props) return; + const { reverseImageSearchType, itemHref, itemSrc } = props; + + if (!reverseImageSearchType || reverseImageSearchType !== "img") return; + + const src = itemHref ?? 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))} + /> + + )); + } +}; + +export default definePlugin({ + name: "ReverseImageSearch", + description: "Adds ImageSearch to image context menus", + authors: [Devs.Ven, Devs.Nuckyz], + tags: ["ImageUtilities"], + + patches: [ + { + find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", + replacement: { + match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/, + replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),` + } + } + ], + + start() { + addContextMenuPatch("message", imageContextMenuPatch); + }, + + stop() { + removeContextMenuPatch("message", imageContextMenuPatch); + } +}); diff --git a/src/plugins/roleColorEverywhere.tsx b/src/plugins/roleColorEverywhere.tsx deleted file mode 100644 index 8b256f4..0000000 --- a/src/plugins/roleColorEverywhere.tsx +++ /dev/null @@ -1,126 +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 { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common"; - -const settings = definePluginSettings({ - chatMentions: { - type: OptionType.BOOLEAN, - default: true, - description: "Show role colors in chat mentions (including in the message box)", - restartNeeded: true - }, - memberList: { - type: OptionType.BOOLEAN, - default: true, - description: "Show role colors in member list role headers", - restartNeeded: true - }, - voiceUsers: { - type: OptionType.BOOLEAN, - default: true, - description: "Show role colors in the voice chat user list", - restartNeeded: true - } -}); - -export default definePlugin({ - name: "RoleColorEverywhere", - authors: [Devs.KingFish, Devs.lewisakura], - description: "Adds the top role color anywhere possible", - patches: [ - // Chat Mentions - { - find: 'className:"mention"', - replacement: [ - { - match: /user:(\i),channel:(\i).{0,300}?"@"\.concat\(.+?\)/, - replace: "$&,color:$self.getUserColor($1?.id,{channelId:$2?.id})" - } - ], - predicate: () => settings.store.chatMentions, - }, - // Slate - { - // taken from CommandsAPI - find: ".source,children", - replacement: [ - { - match: /function \i\((\i)\).{5,20}id.{5,20}guildId.{5,10}channelId.{100,150}hidePersonalInformation.{5,50}jsx.{5,20},{/, - replace: "$&color:$self.getUserColor($1.id,{guildId:$1?.guildId})," - } - ], - predicate: () => settings.store.chatMentions, - }, - // Member List Role Names - { - find: ".memberGroupsPlaceholder", - replacement: [ - { - match: /(memo\(\(function\((\i)\).{300,500}CHANNEL_MEMBERS_A11Y_LABEL.{100,200}roleIcon.{5,20}null,).," \u2014 ",.\]/, - replace: "$1$self.roleGroupColor($2)]" - }, - ], - predicate: () => settings.store.memberList, - }, - // Voice chat users - { - find: "renderPrioritySpeaker", - replacement: [ - { - match: /renderName=function\(\).{50,75}speaking.{50,100}jsx.{5,10}{/, - replace: "$&...$self.getVoiceProps(this.props)," - } - ], - predicate: () => settings.store.voiceUsers, - } - ], - settings, - - getColor(userId: string, { channelId, guildId }: { channelId?: string; guildId?: string; }) { - if (!(guildId ??= ChannelStore.getChannel(channelId!)?.guild_id)) return null; - return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null; - }, - - getUserColor(userId: string, ids: { channelId?: string; guildId?: string; }) { - const colorString = this.getColor(userId, ids); - return colorString && parseInt(colorString.slice(1), 16); - }, - - roleGroupColor({ id, count, title, guildId }: { id: string; count: number; title: string; guildId: string; }) { - const guild = GuildStore.getGuild(guildId); - const role = guild?.roles[id]; - - return {title} — {count}; - }, - - getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) { - return { - style: { - color: this.getColor(userId, { guildId }) - } - }; - } -}); diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx new file mode 100644 index 0000000..8b256f4 --- /dev/null +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -0,0 +1,126 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common"; + +const settings = definePluginSettings({ + chatMentions: { + type: OptionType.BOOLEAN, + default: true, + description: "Show role colors in chat mentions (including in the message box)", + restartNeeded: true + }, + memberList: { + type: OptionType.BOOLEAN, + default: true, + description: "Show role colors in member list role headers", + restartNeeded: true + }, + voiceUsers: { + type: OptionType.BOOLEAN, + default: true, + description: "Show role colors in the voice chat user list", + restartNeeded: true + } +}); + +export default definePlugin({ + name: "RoleColorEverywhere", + authors: [Devs.KingFish, Devs.lewisakura], + description: "Adds the top role color anywhere possible", + patches: [ + // Chat Mentions + { + find: 'className:"mention"', + replacement: [ + { + match: /user:(\i),channel:(\i).{0,300}?"@"\.concat\(.+?\)/, + replace: "$&,color:$self.getUserColor($1?.id,{channelId:$2?.id})" + } + ], + predicate: () => settings.store.chatMentions, + }, + // Slate + { + // taken from CommandsAPI + find: ".source,children", + replacement: [ + { + match: /function \i\((\i)\).{5,20}id.{5,20}guildId.{5,10}channelId.{100,150}hidePersonalInformation.{5,50}jsx.{5,20},{/, + replace: "$&color:$self.getUserColor($1.id,{guildId:$1?.guildId})," + } + ], + predicate: () => settings.store.chatMentions, + }, + // Member List Role Names + { + find: ".memberGroupsPlaceholder", + replacement: [ + { + match: /(memo\(\(function\((\i)\).{300,500}CHANNEL_MEMBERS_A11Y_LABEL.{100,200}roleIcon.{5,20}null,).," \u2014 ",.\]/, + replace: "$1$self.roleGroupColor($2)]" + }, + ], + predicate: () => settings.store.memberList, + }, + // Voice chat users + { + find: "renderPrioritySpeaker", + replacement: [ + { + match: /renderName=function\(\).{50,75}speaking.{50,100}jsx.{5,10}{/, + replace: "$&...$self.getVoiceProps(this.props)," + } + ], + predicate: () => settings.store.voiceUsers, + } + ], + settings, + + getColor(userId: string, { channelId, guildId }: { channelId?: string; guildId?: string; }) { + if (!(guildId ??= ChannelStore.getChannel(channelId!)?.guild_id)) return null; + return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null; + }, + + getUserColor(userId: string, ids: { channelId?: string; guildId?: string; }) { + const colorString = this.getColor(userId, ids); + return colorString && parseInt(colorString.slice(1), 16); + }, + + roleGroupColor({ id, count, title, guildId }: { id: string; count: number; title: string; guildId: string; }) { + const guild = GuildStore.getGuild(guildId); + const role = guild?.roles[id]; + + return {title} — {count}; + }, + + getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) { + return { + style: { + color: this.getColor(userId, { guildId }) + } + }; + } +}); diff --git a/src/plugins/secretRingTone.ts b/src/plugins/secretRingTone.ts deleted file mode 100644 index 0b0b7e3..0000000 --- a/src/plugins/secretRingTone.ts +++ /dev/null @@ -1,35 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "SecretRingToneEnabler", - description: "Always play the secret version of the discord ringtone", - authors: [Devs.AndrewDLO], - patches: [ - { - find: "84a1b4e11d634dbfa1e5dd97a96de3ad", - replacement: { - match: "84a1b4e11d634dbfa1e5dd97a96de3ad.mp3", - replace: "b9411af07f154a6fef543e7e442e4da9.mp3", - }, - }, - ], -}); diff --git a/src/plugins/secretRingTone/index.ts b/src/plugins/secretRingTone/index.ts new file mode 100644 index 0000000..0b0b7e3 --- /dev/null +++ b/src/plugins/secretRingTone/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: "SecretRingToneEnabler", + description: "Always play the secret version of the discord ringtone", + authors: [Devs.AndrewDLO], + patches: [ + { + find: "84a1b4e11d634dbfa1e5dd97a96de3ad", + replacement: { + match: "84a1b4e11d634dbfa1e5dd97a96de3ad.mp3", + replace: "b9411af07f154a6fef543e7e442e4da9.mp3", + }, + }, + ], +}); diff --git a/src/plugins/serverListIndicators.tsx b/src/plugins/serverListIndicators.tsx deleted file mode 100644 index 96833d8..0000000 --- a/src/plugins/serverListIndicators.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Sofia Lima - * - * 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 { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; -import { Settings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { useForceUpdater } from "@utils/react"; -import definePlugin, { OptionType } from "@utils/types"; -import { GuildStore, PresenceStore, RelationshipStore } from "@webpack/common"; - -const enum IndicatorType { - SERVER = 1 << 0, - FRIEND = 1 << 1, - BOTH = SERVER | FRIEND, -} - -let onlineFriends = 0; -let guildCount = 0; -let forceUpdateFriendCount: () => void; -let forceUpdateGuildCount: () => void; - -function FriendsIndicator() { - forceUpdateFriendCount = useForceUpdater(); - - return ( - - {onlineFriends} online - - ); -} - -function ServersIndicator() { - forceUpdateGuildCount = useForceUpdater(); - - return ( - - {guildCount} servers - - ); -} - -function handlePresenceUpdate() { - onlineFriends = 0; - const relations = RelationshipStore.getRelationships(); - for (const id of Object.keys(relations)) { - const type = relations[id]; - // FRIEND relationship type - if (type === 1 && PresenceStore.getStatus(id) !== "offline") { - onlineFriends += 1; - } - } - forceUpdateFriendCount?.(); -} - -function handleGuildUpdate() { - guildCount = GuildStore.getGuildCount(); - forceUpdateGuildCount?.(); -} - -export default definePlugin({ - name: "ServerListIndicators", - description: "Add online friend count or server count in the server list", - authors: [Devs.dzshn], - dependencies: ["ServerListAPI"], - - options: { - mode: { - description: "mode", - type: OptionType.SELECT, - options: [ - { label: "Only online friend count", value: IndicatorType.FRIEND, default: true }, - { label: "Only server count", value: IndicatorType.SERVER }, - { label: "Both server and online friend counts", value: IndicatorType.BOTH }, - ] - } - }, - - renderIndicator: () => { - const { mode } = Settings.plugins.ServerListIndicators; - return -
- {!!(mode & IndicatorType.FRIEND) && } - {!!(mode & IndicatorType.SERVER) && } -
-
; - }, - - flux: { - PRESENCE_UPDATES: handlePresenceUpdate, - GUILD_CREATE: handleGuildUpdate, - GUILD_DELETE: handleGuildUpdate, - }, - - - start() { - addServerListElement(ServerListRenderPosition.Above, this.renderIndicator); - - handlePresenceUpdate(); - handleGuildUpdate(); - }, - - stop() { - removeServerListElement(ServerListRenderPosition.Above, this.renderIndicator); - } -}); diff --git a/src/plugins/serverListIndicators/index.tsx b/src/plugins/serverListIndicators/index.tsx new file mode 100644 index 0000000..96833d8 --- /dev/null +++ b/src/plugins/serverListIndicators/index.tsx @@ -0,0 +1,137 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Sofia Lima + * + * 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 { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; +import { Settings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { useForceUpdater } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { GuildStore, PresenceStore, RelationshipStore } from "@webpack/common"; + +const enum IndicatorType { + SERVER = 1 << 0, + FRIEND = 1 << 1, + BOTH = SERVER | FRIEND, +} + +let onlineFriends = 0; +let guildCount = 0; +let forceUpdateFriendCount: () => void; +let forceUpdateGuildCount: () => void; + +function FriendsIndicator() { + forceUpdateFriendCount = useForceUpdater(); + + return ( + + {onlineFriends} online + + ); +} + +function ServersIndicator() { + forceUpdateGuildCount = useForceUpdater(); + + return ( + + {guildCount} servers + + ); +} + +function handlePresenceUpdate() { + onlineFriends = 0; + const relations = RelationshipStore.getRelationships(); + for (const id of Object.keys(relations)) { + const type = relations[id]; + // FRIEND relationship type + if (type === 1 && PresenceStore.getStatus(id) !== "offline") { + onlineFriends += 1; + } + } + forceUpdateFriendCount?.(); +} + +function handleGuildUpdate() { + guildCount = GuildStore.getGuildCount(); + forceUpdateGuildCount?.(); +} + +export default definePlugin({ + name: "ServerListIndicators", + description: "Add online friend count or server count in the server list", + authors: [Devs.dzshn], + dependencies: ["ServerListAPI"], + + options: { + mode: { + description: "mode", + type: OptionType.SELECT, + options: [ + { label: "Only online friend count", value: IndicatorType.FRIEND, default: true }, + { label: "Only server count", value: IndicatorType.SERVER }, + { label: "Both server and online friend counts", value: IndicatorType.BOTH }, + ] + } + }, + + renderIndicator: () => { + const { mode } = Settings.plugins.ServerListIndicators; + return +
+ {!!(mode & IndicatorType.FRIEND) && } + {!!(mode & IndicatorType.SERVER) && } +
+
; + }, + + flux: { + PRESENCE_UPDATES: handlePresenceUpdate, + GUILD_CREATE: handleGuildUpdate, + GUILD_DELETE: handleGuildUpdate, + }, + + + start() { + addServerListElement(ServerListRenderPosition.Above, this.renderIndicator); + + handlePresenceUpdate(); + handleGuildUpdate(); + }, + + stop() { + removeServerListElement(ServerListRenderPosition.Above, this.renderIndicator); + } +}); diff --git a/src/plugins/showAllMessageButtons.ts b/src/plugins/showAllMessageButtons.ts deleted file mode 100644 index da0d3c6..0000000 --- a/src/plugins/showAllMessageButtons.ts +++ /dev/null @@ -1,37 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "ShowAllMessageButtons", - description: "Always show all message buttons no matter if you are holding the shift key or not.", - authors: [Devs.Nuckyz], - - patches: [ - { - find: ".Messages.MESSAGE_UTILITIES_A11Y_LABEL", - replacement: { - // isExpanded: V, (?<=,V = shiftKeyDown && !H...,|;) - match: /isExpanded:(\i),(?<=,\1=\i&&(?=(!.+?)[,;]).+?)/, - replace: "isExpanded:$2," - } - } - ] -}); diff --git a/src/plugins/showAllMessageButtons/index.ts b/src/plugins/showAllMessageButtons/index.ts new file mode 100644 index 0000000..da0d3c6 --- /dev/null +++ b/src/plugins/showAllMessageButtons/index.ts @@ -0,0 +1,37 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "ShowAllMessageButtons", + description: "Always show all message buttons no matter if you are holding the shift key or not.", + authors: [Devs.Nuckyz], + + patches: [ + { + find: ".Messages.MESSAGE_UTILITIES_A11Y_LABEL", + replacement: { + // isExpanded: V, (?<=,V = shiftKeyDown && !H...,|;) + match: /isExpanded:(\i),(?<=,\1=\i&&(?=(!.+?)[,;]).+?)/, + replace: "isExpanded:$2," + } + } + ] +}); diff --git a/src/plugins/showTimeouts.ts b/src/plugins/showTimeouts.ts deleted file mode 100644 index b0774be..0000000 --- a/src/plugins/showTimeouts.ts +++ /dev/null @@ -1,35 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "ShowTimeouts", - description: "Display member timeout icons in chat regardless of permissions.", - authors: [Devs.Dolfies], - patches: [ - { - find: "showCommunicationDisabledStyles", - replacement: { - match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/, - replace: "", - }, - }, - ], -}); diff --git a/src/plugins/showTimeouts/index.ts b/src/plugins/showTimeouts/index.ts new file mode 100644 index 0000000..b0774be --- /dev/null +++ b/src/plugins/showTimeouts/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: "ShowTimeouts", + description: "Display member timeout icons in chat regardless of permissions.", + authors: [Devs.Dolfies], + patches: [ + { + find: "showCommunicationDisabledStyles", + replacement: { + match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/, + replace: "", + }, + }, + ], +}); diff --git a/src/plugins/silentMessageToggle.tsx b/src/plugins/silentMessageToggle.tsx deleted file mode 100644 index 22d2c05..0000000 --- a/src/plugins/silentMessageToggle.tsx +++ /dev/null @@ -1,120 +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 { 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"; - -let lastState = false; - -const settings = definePluginSettings({ - persistState: { - type: OptionType.BOOLEAN, - description: "Whether to persist the state of the silent message toggle when changing channels", - default: false, - onChange(newValue: boolean) { - if (newValue === false) lastState = false; - } - }, - autoDisable: { - type: OptionType.BOOLEAN, - description: "Automatically disable the silent message toggle again after sending one", - default: true - } -}); - -function SilentMessageToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const [enabled, setEnabled] = React.useState(lastState); - - function setEnabledValue(value: boolean) { - if (settings.store.persistState) lastState = value; - setEnabled(value); - } - - React.useEffect(() => { - const listener: SendListener = (_, message) => { - if (enabled) { - if (settings.store.autoDisable) setEnabledValue(false); - if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content; - } - }; - - addPreSendListener(listener); - return () => void removePreSendListener(listener); - }, [enabled]); - - if (chatBoxProps.type.analyticsName !== "normal") return null; - - return ( - - {tooltipProps => ( -
- -
- )} -
- ); -} - -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"], - - settings, - patches: [ - { - find: ".activeCommandOption", - replacement: { - match: /"gift"\)\);(?<=(\i)\.push.+?disabled:(\i),.+?)/, - replace: (m, array, disabled) => `${m};try{${disabled}||${array}.push($self.SilentMessageToggle(arguments[0]));}catch{}` - } - } - ], - - SilentMessageToggle: ErrorBoundary.wrap(SilentMessageToggle, { noop: true }), -}); diff --git a/src/plugins/silentMessageToggle/index.tsx b/src/plugins/silentMessageToggle/index.tsx new file mode 100644 index 0000000..22d2c05 --- /dev/null +++ b/src/plugins/silentMessageToggle/index.tsx @@ -0,0 +1,120 @@ +/* + * 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 { 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"; + +let lastState = false; + +const settings = definePluginSettings({ + persistState: { + type: OptionType.BOOLEAN, + description: "Whether to persist the state of the silent message toggle when changing channels", + default: false, + onChange(newValue: boolean) { + if (newValue === false) lastState = false; + } + }, + autoDisable: { + type: OptionType.BOOLEAN, + description: "Automatically disable the silent message toggle again after sending one", + default: true + } +}); + +function SilentMessageToggle(chatBoxProps: { + type: { + analyticsName: string; + }; +}) { + const [enabled, setEnabled] = React.useState(lastState); + + function setEnabledValue(value: boolean) { + if (settings.store.persistState) lastState = value; + setEnabled(value); + } + + React.useEffect(() => { + const listener: SendListener = (_, message) => { + if (enabled) { + if (settings.store.autoDisable) setEnabledValue(false); + if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content; + } + }; + + addPreSendListener(listener); + return () => void removePreSendListener(listener); + }, [enabled]); + + if (chatBoxProps.type.analyticsName !== "normal") return null; + + return ( + + {tooltipProps => ( +
+ +
+ )} +
+ ); +} + +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"], + + settings, + patches: [ + { + find: ".activeCommandOption", + replacement: { + match: /"gift"\)\);(?<=(\i)\.push.+?disabled:(\i),.+?)/, + replace: (m, array, disabled) => `${m};try{${disabled}||${array}.push($self.SilentMessageToggle(arguments[0]));}catch{}` + } + } + ], + + SilentMessageToggle: ErrorBoundary.wrap(SilentMessageToggle, { noop: true }), +}); diff --git a/src/plugins/silentTyping.tsx b/src/plugins/silentTyping.tsx deleted file mode 100644 index a4dc256..0000000 --- a/src/plugins/silentTyping.tsx +++ /dev/null @@ -1,124 +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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; -import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, React, Tooltip } from "@webpack/common"; - -const settings = definePluginSettings({ - showIcon: { - type: OptionType.BOOLEAN, - default: false, - description: "Show an icon for toggling the plugin", - restartNeeded: true, - }, - isEnabled: { - type: OptionType.BOOLEAN, - description: "Toggle functionality", - default: true, - } -}); - -function SilentTypingToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const { isEnabled } = settings.use(["isEnabled"]); - const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; - - if (chatBoxProps.type.analyticsName !== "normal") return null; - - return ( - - {(tooltipProps: any) => ( -
- -
- )} -
- ); -} - -export default definePlugin({ - name: "SilentTyping", - authors: [Devs.Ven, Devs.dzshn], - description: "Hide that you are typing", - patches: [ - { - find: "startTyping:", - replacement: { - match: /startTyping:.+?,stop/, - replace: "startTyping:$self.startTyping,stop" - } - }, - { - find: ".activeCommandOption", - predicate: () => settings.store.showIcon, - replacement: { - match: /(.)\.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.", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "value", - description: "whether to hide or not that you're typing (default is toggle)", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, - }, - ], - execute: async (args, ctx) => { - settings.store.isEnabled = !!findOption(args, "value", !settings.store.isEnabled); - sendBotMessage(ctx.channel.id, { - content: settings.store.isEnabled ? "Silent typing enabled!" : "Silent typing disabled!", - }); - }, - }], - - async startTyping(channelId: string) { - if (settings.store.isEnabled) return; - FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); - }, - - chatBarIcon: ErrorBoundary.wrap(SilentTypingToggle, { noop: true }), -}); diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx new file mode 100644 index 0000000..a4dc256 --- /dev/null +++ b/src/plugins/silentTyping/index.tsx @@ -0,0 +1,124 @@ +/* + * 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, React, Tooltip } from "@webpack/common"; + +const settings = definePluginSettings({ + showIcon: { + type: OptionType.BOOLEAN, + default: false, + description: "Show an icon for toggling the plugin", + restartNeeded: true, + }, + isEnabled: { + type: OptionType.BOOLEAN, + description: "Toggle functionality", + default: true, + } +}); + +function SilentTypingToggle(chatBoxProps: { + type: { + analyticsName: string; + }; +}) { + const { isEnabled } = settings.use(["isEnabled"]); + const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; + + if (chatBoxProps.type.analyticsName !== "normal") return null; + + return ( + + {(tooltipProps: any) => ( +
+ +
+ )} +
+ ); +} + +export default definePlugin({ + name: "SilentTyping", + authors: [Devs.Ven, Devs.dzshn], + description: "Hide that you are typing", + patches: [ + { + find: "startTyping:", + replacement: { + match: /startTyping:.+?,stop/, + replace: "startTyping:$self.startTyping,stop" + } + }, + { + find: ".activeCommandOption", + predicate: () => settings.store.showIcon, + replacement: { + match: /(.)\.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.", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [ + { + name: "value", + description: "whether to hide or not that you're typing (default is toggle)", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + ], + execute: async (args, ctx) => { + settings.store.isEnabled = !!findOption(args, "value", !settings.store.isEnabled); + sendBotMessage(ctx.channel.id, { + content: settings.store.isEnabled ? "Silent typing enabled!" : "Silent typing disabled!", + }); + }, + }], + + async startTyping(channelId: string) { + if (settings.store.isEnabled) return; + FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); + }, + + chatBarIcon: ErrorBoundary.wrap(SilentTypingToggle, { noop: true }), +}); diff --git a/src/plugins/sortFriendRequests.tsx b/src/plugins/sortFriendRequests.tsx deleted file mode 100644 index b9732af..0000000 --- a/src/plugins/sortFriendRequests.tsx +++ /dev/null @@ -1,74 +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 { Flex } from "@components/Flex"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { RelationshipStore } from "@webpack/common"; -import { User } from "discord-types/general"; -import { Settings } from "Vencord"; - -export default definePlugin({ - name: "SortFriendRequests", - authors: [Devs.Megu], - description: "Sorts friend requests by date of receipt", - - patches: [{ - find: ".PENDING_INCOMING||", - replacement: [{ - match: /\.sortBy\(\(function\((\w)\){return \w{1,3}\.comparator}\)\)/, - // If the row type is 3 or 4 (pendinng incoming or outgoing), sort by date of receipt - // Otherwise, use the default comparator - replace: (_, row) => `.sortBy((function(${row}) { - return ${row}.type === 3 || ${row}.type === 4 - ? -Vencord.Plugins.plugins.SortFriendRequests.getSince(${row}.user) - : ${row}.comparator - }))` - }, { - predicate: () => Settings.plugins.SortFriendRequests.showDates, - match: /(user:(\w{1,3}),.{10,30}),subText:(\w{1,3}),(.{10,30}userInfo}\))/, - // Show dates in the friend request list - replace: (_, pre, user, subText, post) => `${pre}, - subText: Vencord.Plugins.plugins.SortFriendRequests.makeSubtext(${subText}, ${user}), - ${post}` - }] - }], - - getSince(user: User) { - return new Date(RelationshipStore.getSince(user.id)); - }, - - makeSubtext(text: string, user: User) { - const since = this.getSince(user); - return ( - - {text} - {!isNaN(since.getTime()) && Received — {since.toDateString()}} - - ); - }, - - options: { - showDates: { - type: OptionType.BOOLEAN, - description: "Show dates on friend requests", - default: false, - restartNeeded: true - } - } -}); diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx new file mode 100644 index 0000000..b9732af --- /dev/null +++ b/src/plugins/sortFriendRequests/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 { Flex } from "@components/Flex"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { RelationshipStore } from "@webpack/common"; +import { User } from "discord-types/general"; +import { Settings } from "Vencord"; + +export default definePlugin({ + name: "SortFriendRequests", + authors: [Devs.Megu], + description: "Sorts friend requests by date of receipt", + + patches: [{ + find: ".PENDING_INCOMING||", + replacement: [{ + match: /\.sortBy\(\(function\((\w)\){return \w{1,3}\.comparator}\)\)/, + // If the row type is 3 or 4 (pendinng incoming or outgoing), sort by date of receipt + // Otherwise, use the default comparator + replace: (_, row) => `.sortBy((function(${row}) { + return ${row}.type === 3 || ${row}.type === 4 + ? -Vencord.Plugins.plugins.SortFriendRequests.getSince(${row}.user) + : ${row}.comparator + }))` + }, { + predicate: () => Settings.plugins.SortFriendRequests.showDates, + match: /(user:(\w{1,3}),.{10,30}),subText:(\w{1,3}),(.{10,30}userInfo}\))/, + // Show dates in the friend request list + replace: (_, pre, user, subText, post) => `${pre}, + subText: Vencord.Plugins.plugins.SortFriendRequests.makeSubtext(${subText}, ${user}), + ${post}` + }] + }], + + getSince(user: User) { + return new Date(RelationshipStore.getSince(user.id)); + }, + + makeSubtext(text: string, user: User) { + const since = this.getSince(user); + return ( + + {text} + {!isNaN(since.getTime()) && Received — {since.toDateString()}} + + ); + }, + + options: { + showDates: { + type: OptionType.BOOLEAN, + description: "Show dates on friend requests", + default: false, + restartNeeded: true + } + } +}); diff --git a/src/plugins/spotifyCrack.ts b/src/plugins/spotifyCrack.ts deleted file mode 100644 index f02df72..0000000 --- a/src/plugins/spotifyCrack.ts +++ /dev/null @@ -1,69 +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 { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const settings = definePluginSettings({ - noSpotifyAutoPause: { - description: "Disable Spotify auto-pause", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true - }, - keepSpotifyActivityOnIdle: { - description: "Keep Spotify activity playing when idling", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: true - } -}); - -export default definePlugin({ - name: "SpotifyCrack", - description: "Free listen along, no auto-pausing in voice chat, and allows activity to continue playing when idling", - authors: [Devs.Cyn, Devs.Nuckyz], - settings, - - patches: [ - { - - find: 'dispatch({type:"SPOTIFY_PROFILE_UPDATE"', - replacement: { - match: /SPOTIFY_PROFILE_UPDATE.+?isPremium:(?="premium"===(\i)\.body\.product)/, - replace: (m, req) => `${m}(${req}.body.product="premium")&&` - }, - }, - { - find: '.displayName="SpotifyStore"', - replacement: [ - { - predicate: () => settings.store.noSpotifyAutoPause, - match: /(?<=function \i\(\){)(?=.{0,200}SPOTIFY_AUTO_PAUSED\))/, - replace: "return;" - }, - { - predicate: () => settings.store.keepSpotifyActivityOnIdle, - match: /(?<=shouldShowActivity=function\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/, - replace: "" - } - ] - } - ] -}); diff --git a/src/plugins/spotifyCrack/index.ts b/src/plugins/spotifyCrack/index.ts new file mode 100644 index 0000000..f02df72 --- /dev/null +++ b/src/plugins/spotifyCrack/index.ts @@ -0,0 +1,69 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +const settings = definePluginSettings({ + noSpotifyAutoPause: { + description: "Disable Spotify auto-pause", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + }, + keepSpotifyActivityOnIdle: { + description: "Keep Spotify activity playing when idling", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + } +}); + +export default definePlugin({ + name: "SpotifyCrack", + description: "Free listen along, no auto-pausing in voice chat, and allows activity to continue playing when idling", + authors: [Devs.Cyn, Devs.Nuckyz], + settings, + + patches: [ + { + + find: 'dispatch({type:"SPOTIFY_PROFILE_UPDATE"', + replacement: { + match: /SPOTIFY_PROFILE_UPDATE.+?isPremium:(?="premium"===(\i)\.body\.product)/, + replace: (m, req) => `${m}(${req}.body.product="premium")&&` + }, + }, + { + find: '.displayName="SpotifyStore"', + replacement: [ + { + predicate: () => settings.store.noSpotifyAutoPause, + match: /(?<=function \i\(\){)(?=.{0,200}SPOTIFY_AUTO_PAUSED\))/, + replace: "return;" + }, + { + predicate: () => settings.store.keepSpotifyActivityOnIdle, + match: /(?<=shouldShowActivity=function\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/, + replace: "" + } + ] + } + ] +}); diff --git a/src/plugins/spotifyShareCommands.ts b/src/plugins/spotifyShareCommands.ts deleted file mode 100644 index 7634e9d..0000000 --- a/src/plugins/spotifyShareCommands.ts +++ /dev/null @@ -1,138 +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 { ApplicationCommandInputType, sendBotMessage } from "@api/Commands"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { FluxDispatcher } from "@webpack/common"; - -interface Album { - id: string; - image: { - height: number; - width: number; - url: string; - }; - name: string; -} - -interface Artist { - external_urls: { - spotify: string; - }; - href: string; - id: string; - name: string; - type: "artist" | string; - uri: string; -} - -interface Track { - id: string; - album: Album; - artists: Artist[]; - duration: number; - isLocal: boolean; - name: string; -} - -const Spotify = findByPropsLazy("getPlayerState"); -const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage"); -const PendingReplyStore = findByPropsLazy("getPendingReply"); - -function sendMessage(channelId, message) { - message = { - // The following are required to prevent Discord from throwing an error - invalidEmojis: [], - tts: false, - validNonShortcutEmojis: [], - ...message - }; - const reply = PendingReplyStore.getPendingReply(channelId); - MessageCreator.sendMessage(channelId, message, void 0, MessageCreator.getSendMessageOptionsForReply(reply)) - .then(() => { - if (reply) { - FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId }); - } - }); -} - -export default definePlugin({ - name: "SpotifyShareCommands", - description: "Share your current Spotify track, album or artist via slash command (/track, /album, /artist)", - authors: [Devs.katlyn], - dependencies: ["CommandsAPI"], - commands: [ - { - name: "track", - description: "Send your current Spotify track to chat", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [], - execute: (_, ctx) => { - const track: Track | null = Spotify.getTrack(); - if (track === null) { - sendBotMessage(ctx.channel.id, { - content: "You're not listening to any music." - }); - return; - } - // Note: Due to how Discord handles commands, we need to manually create and send the message - sendMessage(ctx.channel.id, { - content: `https://open.spotify.com/track/${track.id}` - }); - } - }, - { - name: "album", - description: "Send your current Spotify album to chat", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [], - execute: (_, ctx) => { - const track: Track | null = Spotify.getTrack(); - if (track === null) { - sendBotMessage(ctx.channel.id, { - content: "You're not listening to any music." - }); - return; - } - sendMessage(ctx.channel.id, { - content: `https://open.spotify.com/album/${track.album.id}` - }); - } - }, - { - name: "artist", - description: "Send your current Spotify artist to chat", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [], - execute: (_, ctx) => { - const track: Track | null = Spotify.getTrack(); - if (track === null) { - sendBotMessage(ctx.channel.id, { - content: "You're not listening to any music." - }); - return; - } - sendMessage(ctx.channel.id, { - content: track.artists[0].external_urls.spotify - }); - } - } - ] -}); diff --git a/src/plugins/spotifyShareCommands/index.ts b/src/plugins/spotifyShareCommands/index.ts new file mode 100644 index 0000000..7634e9d --- /dev/null +++ b/src/plugins/spotifyShareCommands/index.ts @@ -0,0 +1,138 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { FluxDispatcher } from "@webpack/common"; + +interface Album { + id: string; + image: { + height: number; + width: number; + url: string; + }; + name: string; +} + +interface Artist { + external_urls: { + spotify: string; + }; + href: string; + id: string; + name: string; + type: "artist" | string; + uri: string; +} + +interface Track { + id: string; + album: Album; + artists: Artist[]; + duration: number; + isLocal: boolean; + name: string; +} + +const Spotify = findByPropsLazy("getPlayerState"); +const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage"); +const PendingReplyStore = findByPropsLazy("getPendingReply"); + +function sendMessage(channelId, message) { + message = { + // The following are required to prevent Discord from throwing an error + invalidEmojis: [], + tts: false, + validNonShortcutEmojis: [], + ...message + }; + const reply = PendingReplyStore.getPendingReply(channelId); + MessageCreator.sendMessage(channelId, message, void 0, MessageCreator.getSendMessageOptionsForReply(reply)) + .then(() => { + if (reply) { + FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId }); + } + }); +} + +export default definePlugin({ + name: "SpotifyShareCommands", + description: "Share your current Spotify track, album or artist via slash command (/track, /album, /artist)", + authors: [Devs.katlyn], + dependencies: ["CommandsAPI"], + commands: [ + { + name: "track", + description: "Send your current Spotify track to chat", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [], + execute: (_, ctx) => { + const track: Track | null = Spotify.getTrack(); + if (track === null) { + sendBotMessage(ctx.channel.id, { + content: "You're not listening to any music." + }); + return; + } + // Note: Due to how Discord handles commands, we need to manually create and send the message + sendMessage(ctx.channel.id, { + content: `https://open.spotify.com/track/${track.id}` + }); + } + }, + { + name: "album", + description: "Send your current Spotify album to chat", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [], + execute: (_, ctx) => { + const track: Track | null = Spotify.getTrack(); + if (track === null) { + sendBotMessage(ctx.channel.id, { + content: "You're not listening to any music." + }); + return; + } + sendMessage(ctx.channel.id, { + content: `https://open.spotify.com/album/${track.album.id}` + }); + } + }, + { + name: "artist", + description: "Send your current Spotify artist to chat", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [], + execute: (_, ctx) => { + const track: Track | null = Spotify.getTrack(); + if (track === null) { + sendBotMessage(ctx.channel.id, { + content: "You're not listening to any music." + }); + return; + } + sendMessage(ctx.channel.id, { + content: track.artists[0].external_urls.spotify + }); + } + } + ] +}); diff --git a/src/plugins/textReplace.tsx b/src/plugins/textReplace.tsx deleted file mode 100644 index 45fb6f9..0000000 --- a/src/plugins/textReplace.tsx +++ /dev/null @@ -1,267 +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 { DataStore } from "@api/index"; -import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings } from "@api/Settings"; -import { Flex } from "@components/Flex"; -import { Devs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import { useForceUpdater } from "@utils/react"; -import definePlugin, { OptionType } from "@utils/types"; -import { Button, Forms, React, TextInput, useState } from "@webpack/common"; - -const STRING_RULES_KEY = "TextReplace_rulesString"; -const REGEX_RULES_KEY = "TextReplace_rulesRegex"; - -type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>; - -interface TextReplaceProps { - title: string; - rulesArray: Rule[]; - rulesKey: string; - update: () => void; -} - -const makeEmptyRule: () => Rule = () => ({ - find: "", - replace: "", - onlyIfIncludes: "" -}); -const makeEmptyRuleArray = () => [makeEmptyRule()]; - -let stringRules = makeEmptyRuleArray(); -let regexRules = makeEmptyRuleArray(); - -const settings = definePluginSettings({ - replace: { - type: OptionType.COMPONENT, - description: "", - component: () => { - const update = useForceUpdater(); - return ( - <> - - - - - ); - } - }, -}); - -function stringToRegex(str: string) { - const match = str.match(/^(\/)?(.+?)(?:\/([gimsuy]*))?$/); // Regex to match regex - return match - ? new RegExp( - match[2], // Pattern - match[3] - ?.split("") // Remove duplicate flags - .filter((char, pos, flagArr) => flagArr.indexOf(char) === pos) - .join("") - ?? "g" - ) - : new RegExp(str); // Not a regex, return string -} - -function renderFindError(find: string) { - try { - stringToRegex(find); - return null; - } catch (e) { - return ( - - {String(e)} - - ); - } -} - -function Input({ initialValue, onChange, placeholder }: { - placeholder: string; - initialValue: string; - onChange(value: string): void; -}) { - const [value, setValue] = useState(initialValue); - return ( - value !== initialValue && onChange(value)} - /> - ); -} - -function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) { - const isRegexRules = title === "Using Regex"; - - async function onClickRemove(index: number) { - if (index === rulesArray.length - 1) return; - rulesArray.splice(index, 1); - - await DataStore.set(rulesKey, rulesArray); - update(); - } - - async function onChange(e: string, index: number, key: string) { - if (index === rulesArray.length - 1) - rulesArray.push(makeEmptyRule()); - - rulesArray[index][key] = e; - - if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) - rulesArray.splice(index, 1); - - await DataStore.set(rulesKey, rulesArray); - update(); - } - - return ( - <> - {title} - - { - rulesArray.map((rule, index) => - - - - onChange(e, index, "find")} - /> - onChange(e, index, "replace")} - /> - onChange(e, index, "onlyIfIncludes")} - /> - - - - {isRegexRules && renderFindError(rule.find)} - - ) - } - - - ); -} - -function TextReplaceTesting() { - const [value, setValue] = useState(""); - return ( - <> - Test Rules - - - - ); -} - -function applyRules(content: string): string { - if (content.length === 0) - return content; - - if (stringRules) { - for (const rule of stringRules) { - if (!rule.find || !rule.replace) continue; - if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; - - content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, ""); - } - } - - if (regexRules) { - for (const rule of regexRules) { - if (!rule.find || !rule.replace) continue; - if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; - - try { - const regex = stringToRegex(rule.find); - content = content.replace(regex, rule.replace.replaceAll("\\n", "\n")); - } catch (e) { - new Logger("TextReplace").error(`Invalid regex: ${rule.find}`); - } - } - } - - content = content.trim(); - return content; -} - -const TEXT_REPLACE_RULES_CHANNEL_ID = "1102784112584040479"; - -export default definePlugin({ - name: "TextReplace", - description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server", - authors: [Devs.AutumnVN, Devs.TheKodeToad], - dependencies: ["MessageEventsAPI"], - - settings, - - async start() { - stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray(); - regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray(); - - this.preSend = addPreSendListener((channelId, msg) => { - // Channel used for sharing rules, applying rules here would be messy - if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return; - msg.content = applyRules(msg.content); - }); - }, - - stop() { - removePreSendListener(this.preSend); - } -}); diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx new file mode 100644 index 0000000..45fb6f9 --- /dev/null +++ b/src/plugins/textReplace/index.tsx @@ -0,0 +1,267 @@ +/* + * 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 { DataStore } from "@api/index"; +import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; +import { definePluginSettings } from "@api/Settings"; +import { Flex } from "@components/Flex"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import { useForceUpdater } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { Button, Forms, React, TextInput, useState } from "@webpack/common"; + +const STRING_RULES_KEY = "TextReplace_rulesString"; +const REGEX_RULES_KEY = "TextReplace_rulesRegex"; + +type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>; + +interface TextReplaceProps { + title: string; + rulesArray: Rule[]; + rulesKey: string; + update: () => void; +} + +const makeEmptyRule: () => Rule = () => ({ + find: "", + replace: "", + onlyIfIncludes: "" +}); +const makeEmptyRuleArray = () => [makeEmptyRule()]; + +let stringRules = makeEmptyRuleArray(); +let regexRules = makeEmptyRuleArray(); + +const settings = definePluginSettings({ + replace: { + type: OptionType.COMPONENT, + description: "", + component: () => { + const update = useForceUpdater(); + return ( + <> + + + + + ); + } + }, +}); + +function stringToRegex(str: string) { + const match = str.match(/^(\/)?(.+?)(?:\/([gimsuy]*))?$/); // Regex to match regex + return match + ? new RegExp( + match[2], // Pattern + match[3] + ?.split("") // Remove duplicate flags + .filter((char, pos, flagArr) => flagArr.indexOf(char) === pos) + .join("") + ?? "g" + ) + : new RegExp(str); // Not a regex, return string +} + +function renderFindError(find: string) { + try { + stringToRegex(find); + return null; + } catch (e) { + return ( + + {String(e)} + + ); + } +} + +function Input({ initialValue, onChange, placeholder }: { + placeholder: string; + initialValue: string; + onChange(value: string): void; +}) { + const [value, setValue] = useState(initialValue); + return ( + value !== initialValue && onChange(value)} + /> + ); +} + +function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) { + const isRegexRules = title === "Using Regex"; + + async function onClickRemove(index: number) { + if (index === rulesArray.length - 1) return; + rulesArray.splice(index, 1); + + await DataStore.set(rulesKey, rulesArray); + update(); + } + + async function onChange(e: string, index: number, key: string) { + if (index === rulesArray.length - 1) + rulesArray.push(makeEmptyRule()); + + rulesArray[index][key] = e; + + if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) + rulesArray.splice(index, 1); + + await DataStore.set(rulesKey, rulesArray); + update(); + } + + return ( + <> + {title} + + { + rulesArray.map((rule, index) => + + + + onChange(e, index, "find")} + /> + onChange(e, index, "replace")} + /> + onChange(e, index, "onlyIfIncludes")} + /> + + + + {isRegexRules && renderFindError(rule.find)} + + ) + } + + + ); +} + +function TextReplaceTesting() { + const [value, setValue] = useState(""); + return ( + <> + Test Rules + + + + ); +} + +function applyRules(content: string): string { + if (content.length === 0) + return content; + + if (stringRules) { + for (const rule of stringRules) { + if (!rule.find || !rule.replace) continue; + if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; + + content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, ""); + } + } + + if (regexRules) { + for (const rule of regexRules) { + if (!rule.find || !rule.replace) continue; + if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; + + try { + const regex = stringToRegex(rule.find); + content = content.replace(regex, rule.replace.replaceAll("\\n", "\n")); + } catch (e) { + new Logger("TextReplace").error(`Invalid regex: ${rule.find}`); + } + } + } + + content = content.trim(); + return content; +} + +const TEXT_REPLACE_RULES_CHANNEL_ID = "1102784112584040479"; + +export default definePlugin({ + name: "TextReplace", + description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server", + authors: [Devs.AutumnVN, Devs.TheKodeToad], + dependencies: ["MessageEventsAPI"], + + settings, + + async start() { + stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray(); + regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray(); + + this.preSend = addPreSendListener((channelId, msg) => { + // Channel used for sharing rules, applying rules here would be messy + if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return; + msg.content = applyRules(msg.content); + }); + }, + + stop() { + removePreSendListener(this.preSend); + } +}); diff --git a/src/plugins/timeBarAllActivities.ts b/src/plugins/timeBarAllActivities.ts deleted file mode 100644 index 223f182..0000000 --- a/src/plugins/timeBarAllActivities.ts +++ /dev/null @@ -1,35 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "TimeBarAllActivities", - description: "Adds the Spotify time bar to all activities if they have start and end timestamps", - authors: [Devs.obscurity], - patches: [ - { - find: "renderTimeBar=function", - replacement: { - match: /renderTimeBar=function\((.{1,3})\){.{0,50}?var/, - replace: "renderTimeBar=function($1){var" - } - } - ], -}); diff --git a/src/plugins/timeBarAllActivities/index.ts b/src/plugins/timeBarAllActivities/index.ts new file mode 100644 index 0000000..223f182 --- /dev/null +++ b/src/plugins/timeBarAllActivities/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "TimeBarAllActivities", + description: "Adds the Spotify time bar to all activities if they have start and end timestamps", + authors: [Devs.obscurity], + patches: [ + { + find: "renderTimeBar=function", + replacement: { + match: /renderTimeBar=function\((.{1,3})\){.{0,50}?var/, + replace: "renderTimeBar=function($1){var" + } + } + ], +}); diff --git a/src/plugins/typingIndicator.tsx b/src/plugins/typingIndicator.tsx deleted file mode 100644 index 2b4de82..0000000 --- a/src/plugins/typingIndicator.tsx +++ /dev/null @@ -1,140 +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 { definePluginSettings, Settings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { LazyComponent } from "@utils/react"; -import definePlugin, { OptionType } from "@utils/types"; -import { find, findLazy, findStoreLazy } from "@webpack"; -import { ChannelStore, GuildMemberStore, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; - -import { buildSeveralUsers } from "./typingTweaks"; - -const ThreeDots = LazyComponent(() => find(m => m.type?.render?.toString()?.includes("().dots"))); - -const TypingStore = findStoreLazy("TypingStore"); -const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); - -const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING); - -function getDisplayName(guildId: string, userId: string) { - return GuildMemberStore.getNick(guildId, userId) ?? UserStore.getUser(userId).username; -} - -function TypingIndicator({ channelId }: { channelId: string; }) { - const typingUsers: Record = useStateFromStores( - [TypingStore], - () => ({ ...TypingStore.getTypingUsers(channelId) as Record }), - null, - (old, current) => { - const oldKeys = Object.keys(old); - const currentKeys = Object.keys(current); - - return oldKeys.length === currentKeys.length && JSON.stringify(oldKeys) === JSON.stringify(currentKeys); - } - ); - - const guildId = ChannelStore.getChannel(channelId).guild_id; - - if (!settings.store.includeMutedChannels) { - const isChannelMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId); - if (isChannelMuted) return null; - } - - const myId = UserStore.getCurrentUser()?.id; - - const typingUsersArray = Object.keys(typingUsers).filter(id => id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers)); - let tooltipText: string; - - switch (typingUsersArray.length) { - case 0: break; - case 1: { - tooltipText = Formatters.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) }); - break; - } - case 2: { - tooltipText = Formatters.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) }); - break; - } - case 3: { - tooltipText = Formatters.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) }); - break; - } - default: { - tooltipText = Settings.plugins.TypingTweaks.enabled - ? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: typingUsersArray.length - 2 }) - : Formatters.Messages.SEVERAL_USERS_TYPING; - break; - } - } - - if (typingUsersArray.length > 0) { - return ( - - {({ onMouseLeave, onMouseEnter }) => ( -
- -
- )} -
- ); - } - - return null; -} - -const settings = definePluginSettings({ - includeMutedChannels: { - type: OptionType.BOOLEAN, - description: "Whether to show the typing indicator for muted channels.", - default: false - }, - includeBlockedUsers: { - type: OptionType.BOOLEAN, - description: "Whether to show the typing indicator for blocked users.", - default: false - } -}); - -export default definePlugin({ - name: "TypingIndicator", - description: "Adds an indicator if someone is typing on a channel.", - authors: [Devs.Nuckyz, Devs.obscurity], - settings, - - patches: [ - { - find: ".UNREAD_HIGHLIGHT", - replacement: { - match: /\(\).children.+?:null(?<=(\i)=\i\.channel,.+?)/, - replace: (m, channel) => `${m},$self.TypingIndicator(${channel}.id)` - } - } - ], - - TypingIndicator: (channelId: string) => ( - - - - ), -}); diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx new file mode 100644 index 0000000..5f7df47 --- /dev/null +++ b/src/plugins/typingIndicator/index.tsx @@ -0,0 +1,140 @@ +/* + * 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 { definePluginSettings, Settings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { LazyComponent } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { find, findLazy, findStoreLazy } from "@webpack"; +import { ChannelStore, GuildMemberStore, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; + +import { buildSeveralUsers } from "../typingTweaks"; + +const ThreeDots = LazyComponent(() => find(m => m.type?.render?.toString()?.includes("().dots"))); + +const TypingStore = findStoreLazy("TypingStore"); +const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); + +const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING); + +function getDisplayName(guildId: string, userId: string) { + return GuildMemberStore.getNick(guildId, userId) ?? UserStore.getUser(userId).username; +} + +function TypingIndicator({ channelId }: { channelId: string; }) { + const typingUsers: Record = useStateFromStores( + [TypingStore], + () => ({ ...TypingStore.getTypingUsers(channelId) as Record }), + null, + (old, current) => { + const oldKeys = Object.keys(old); + const currentKeys = Object.keys(current); + + return oldKeys.length === currentKeys.length && JSON.stringify(oldKeys) === JSON.stringify(currentKeys); + } + ); + + const guildId = ChannelStore.getChannel(channelId).guild_id; + + if (!settings.store.includeMutedChannels) { + const isChannelMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId); + if (isChannelMuted) return null; + } + + const myId = UserStore.getCurrentUser()?.id; + + const typingUsersArray = Object.keys(typingUsers).filter(id => id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers)); + let tooltipText: string; + + switch (typingUsersArray.length) { + case 0: break; + case 1: { + tooltipText = Formatters.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) }); + break; + } + case 2: { + tooltipText = Formatters.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) }); + break; + } + case 3: { + tooltipText = Formatters.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) }); + break; + } + default: { + tooltipText = Settings.plugins.TypingTweaks.enabled + ? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: typingUsersArray.length - 2 }) + : Formatters.Messages.SEVERAL_USERS_TYPING; + break; + } + } + + if (typingUsersArray.length > 0) { + return ( + + {({ onMouseLeave, onMouseEnter }) => ( +
+ +
+ )} +
+ ); + } + + return null; +} + +const settings = definePluginSettings({ + includeMutedChannels: { + type: OptionType.BOOLEAN, + description: "Whether to show the typing indicator for muted channels.", + default: false + }, + includeBlockedUsers: { + type: OptionType.BOOLEAN, + description: "Whether to show the typing indicator for blocked users.", + default: false + } +}); + +export default definePlugin({ + name: "TypingIndicator", + description: "Adds an indicator if someone is typing on a channel.", + authors: [Devs.Nuckyz, Devs.obscurity], + settings, + + patches: [ + { + find: ".UNREAD_HIGHLIGHT", + replacement: { + match: /\(\).children.+?:null(?<=(\i)=\i\.channel,.+?)/, + replace: (m, channel) => `${m},$self.TypingIndicator(${channel}.id)` + } + } + ], + + TypingIndicator: (channelId: string) => ( + + + + ), +}); diff --git a/src/plugins/typingTweaks.tsx b/src/plugins/typingTweaks.tsx deleted file mode 100644 index b76f493..0000000 --- a/src/plugins/typingTweaks.tsx +++ /dev/null @@ -1,139 +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 } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { openUserProfile } from "@utils/discord"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByCodeLazy } from "@webpack"; -import { GuildMemberStore, React, RelationshipStore } from "@webpack/common"; -import { User } from "discord-types/general"; - -const Avatar = findByCodeLazy(".typingIndicatorRef", "svg"); - -const settings = definePluginSettings({ - showAvatars: { - type: OptionType.BOOLEAN, - default: true, - description: "Show avatars in the typing indicator" - }, - showRoleColors: { - type: OptionType.BOOLEAN, - default: true, - description: "Show role colors in the typing indicator" - }, - alternativeFormatting: { - type: OptionType.BOOLEAN, - default: true, - description: "Show a more useful message when several users are typing" - } -}); - -export function buildSeveralUsers({ a, b, c }: { a: string, b: string, c: number; }) { - return [ - {a}, - ", ", - {b}, - `, and ${c} others are typing...` - ]; -} - -interface Props { - user: User; - guildId: string; -} - -const TypingUser = ErrorBoundary.wrap(function ({ user, guildId }: Props) { - return ( - { - openUserProfile(user.id); - }} - style={{ - display: "grid", - gridAutoFlow: "column", - gap: "4px", - color: settings.store.showRoleColors ? GuildMemberStore.getMember(guildId, user.id)?.colorString : undefined, - cursor: "pointer" - }} - > - {settings.store.showAvatars && ( -
- -
- )} - {GuildMemberStore.getNick(guildId!, user.id) - || (!guildId && RelationshipStore.getNickname(user.id)) - || (user as any).globalName - || user.username - } -
- ); -}, { noop: true }); - -export default definePlugin({ - name: "TypingTweaks", - description: "Show avatars and role colours in the typing indicator", - authors: [Devs.zt], - patches: [ - // Style the indicator and add function call to modify the children before rendering - { - find: "getCooldownTextStyle", - replacement: { - match: /=(\i)\[2];(.+)"aria-atomic":!0,children:(\i)}\)/, - replace: "=$1[2];$2\"aria-atomic\":!0,style:{display:\"grid\",gridAutoFlow:\"column\",gridGap:\"0.25em\"},children:$self.mutateChildren(this.props,$1,$3)})" - } - }, - // Changes the indicator to keep the user object when creating the list of typing users - { - find: "getCooldownTextStyle", - replacement: { - match: /return \i\.\i\.getName\(.,.\.props\.channel\.id,(.)\)/, - replace: "return $1" - } - }, - // Adds the alternative formatting for several users typing - { - find: "getCooldownTextStyle", - replacement: { - match: /((\i)\.length\?.\..\.Messages\.THREE_USERS_TYPING.format\(\{a:(\i),b:(\i),c:.}\)):.+?SEVERAL_USERS_TYPING/, - replace: "$1:$self.buildSeveralUsers({a:$3,b:$4,c:$2.length-2})" - }, - predicate: () => settings.store.alternativeFormatting - } - ], - settings, - - buildSeveralUsers, - - mutateChildren(props: any, users: User[], children: any) { - if (!Array.isArray(children)) return children; - - let element = 0; - - return children.map(c => - c.type === "strong" - ? - : c - ); - }, -}); diff --git a/src/plugins/typingTweaks/index.tsx b/src/plugins/typingTweaks/index.tsx new file mode 100644 index 0000000..b76f493 --- /dev/null +++ b/src/plugins/typingTweaks/index.tsx @@ -0,0 +1,139 @@ +/* + * 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 ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { openUserProfile } from "@utils/discord"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy } from "@webpack"; +import { GuildMemberStore, React, RelationshipStore } from "@webpack/common"; +import { User } from "discord-types/general"; + +const Avatar = findByCodeLazy(".typingIndicatorRef", "svg"); + +const settings = definePluginSettings({ + showAvatars: { + type: OptionType.BOOLEAN, + default: true, + description: "Show avatars in the typing indicator" + }, + showRoleColors: { + type: OptionType.BOOLEAN, + default: true, + description: "Show role colors in the typing indicator" + }, + alternativeFormatting: { + type: OptionType.BOOLEAN, + default: true, + description: "Show a more useful message when several users are typing" + } +}); + +export function buildSeveralUsers({ a, b, c }: { a: string, b: string, c: number; }) { + return [ + {a}, + ", ", + {b}, + `, and ${c} others are typing...` + ]; +} + +interface Props { + user: User; + guildId: string; +} + +const TypingUser = ErrorBoundary.wrap(function ({ user, guildId }: Props) { + return ( + { + openUserProfile(user.id); + }} + style={{ + display: "grid", + gridAutoFlow: "column", + gap: "4px", + color: settings.store.showRoleColors ? GuildMemberStore.getMember(guildId, user.id)?.colorString : undefined, + cursor: "pointer" + }} + > + {settings.store.showAvatars && ( +
+ +
+ )} + {GuildMemberStore.getNick(guildId!, user.id) + || (!guildId && RelationshipStore.getNickname(user.id)) + || (user as any).globalName + || user.username + } +
+ ); +}, { noop: true }); + +export default definePlugin({ + name: "TypingTweaks", + description: "Show avatars and role colours in the typing indicator", + authors: [Devs.zt], + patches: [ + // Style the indicator and add function call to modify the children before rendering + { + find: "getCooldownTextStyle", + replacement: { + match: /=(\i)\[2];(.+)"aria-atomic":!0,children:(\i)}\)/, + replace: "=$1[2];$2\"aria-atomic\":!0,style:{display:\"grid\",gridAutoFlow:\"column\",gridGap:\"0.25em\"},children:$self.mutateChildren(this.props,$1,$3)})" + } + }, + // Changes the indicator to keep the user object when creating the list of typing users + { + find: "getCooldownTextStyle", + replacement: { + match: /return \i\.\i\.getName\(.,.\.props\.channel\.id,(.)\)/, + replace: "return $1" + } + }, + // Adds the alternative formatting for several users typing + { + find: "getCooldownTextStyle", + replacement: { + match: /((\i)\.length\?.\..\.Messages\.THREE_USERS_TYPING.format\(\{a:(\i),b:(\i),c:.}\)):.+?SEVERAL_USERS_TYPING/, + replace: "$1:$self.buildSeveralUsers({a:$3,b:$4,c:$2.length-2})" + }, + predicate: () => settings.store.alternativeFormatting + } + ], + settings, + + buildSeveralUsers, + + mutateChildren(props: any, users: User[], children: any) { + if (!Array.isArray(children)) return children; + + let element = 0; + + return children.map(c => + c.type === "strong" + ? + : c + ); + }, +}); diff --git a/src/plugins/unindent.ts b/src/plugins/unindent.ts deleted file mode 100644 index a197ef4..0000000 --- a/src/plugins/unindent.ts +++ /dev/null @@ -1,67 +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 { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "Unindent", - description: "Trims leading indentation from codeblocks", - authors: [Devs.Ven], - dependencies: ["MessageEventsAPI"], - patches: [ - { - find: "inQuote:", - replacement: { - match: /,content:([^,]+),inQuote/, - replace: (_, content) => `,content:Vencord.Plugins.plugins.Unindent.unindent(${content}),inQuote` - } - } - ], - - unindent(str: string) { - // Users cannot send tabs, they get converted to spaces. However, a bot may send tabs, so convert them to 4 spaces first - str = str.replace(/\t/g, " "); - const minIndent = str.match(/^ *(?=\S)/gm) - ?.reduce((prev, curr) => Math.min(prev, curr.length), Infinity) ?? 0; - - if (!minIndent) return str; - return str.replace(new RegExp(`^ {${minIndent}}`, "gm"), ""); - }, - - unindentMsg(msg: MessageObject) { - msg.content = msg.content.replace(/```(.|\n)*?```/g, m => { - const lines = m.split("\n"); - if (lines.length < 2) return m; // Do not affect inline codeblocks - let suffix = ""; - if (lines[lines.length - 1] === "```") suffix = lines.pop()!; - return `${lines[0]}\n${this.unindent(lines.slice(1).join("\n"))}\n${suffix}`; - }); - }, - - start() { - this.preSend = addPreSendListener((_, msg) => this.unindentMsg(msg)); - this.preEdit = addPreEditListener((_cid, _mid, msg) => this.unindentMsg(msg)); - }, - - stop() { - removePreSendListener(this.preSend); - removePreEditListener(this.preEdit); - } -}); diff --git a/src/plugins/unindent/index.ts b/src/plugins/unindent/index.ts new file mode 100644 index 0000000..a197ef4 --- /dev/null +++ b/src/plugins/unindent/index.ts @@ -0,0 +1,67 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "Unindent", + description: "Trims leading indentation from codeblocks", + authors: [Devs.Ven], + dependencies: ["MessageEventsAPI"], + patches: [ + { + find: "inQuote:", + replacement: { + match: /,content:([^,]+),inQuote/, + replace: (_, content) => `,content:Vencord.Plugins.plugins.Unindent.unindent(${content}),inQuote` + } + } + ], + + unindent(str: string) { + // Users cannot send tabs, they get converted to spaces. However, a bot may send tabs, so convert them to 4 spaces first + str = str.replace(/\t/g, " "); + const minIndent = str.match(/^ *(?=\S)/gm) + ?.reduce((prev, curr) => Math.min(prev, curr.length), Infinity) ?? 0; + + if (!minIndent) return str; + return str.replace(new RegExp(`^ {${minIndent}}`, "gm"), ""); + }, + + unindentMsg(msg: MessageObject) { + msg.content = msg.content.replace(/```(.|\n)*?```/g, m => { + const lines = m.split("\n"); + if (lines.length < 2) return m; // Do not affect inline codeblocks + let suffix = ""; + if (lines[lines.length - 1] === "```") suffix = lines.pop()!; + return `${lines[0]}\n${this.unindent(lines.slice(1).join("\n"))}\n${suffix}`; + }); + }, + + start() { + this.preSend = addPreSendListener((_, msg) => this.unindentMsg(msg)); + this.preEdit = addPreEditListener((_cid, _mid, msg) => this.unindentMsg(msg)); + }, + + stop() { + removePreSendListener(this.preSend); + removePreEditListener(this.preEdit); + } +}); diff --git a/src/plugins/unsuppressEmbeds.tsx b/src/plugins/unsuppressEmbeds.tsx deleted file mode 100644 index a219607..0000000 --- a/src/plugins/unsuppressEmbeds.tsx +++ /dev/null @@ -1,67 +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 { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; -import { ImageInvisible, ImageVisible } from "@components/Icons"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common"; - -const EMBED_SUPPRESSED = 1 << 2; - -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => { - const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0; - if (!isEmbedSuppressed && !embeds.length) return; - - const hasEmbedPerms = channel.isPrivate() || !!(PermissionStore.getChannelPermissions({ id: channel.id }) & PermissionsBits.EMBED_LINKS); - if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return; - - const menuGroup = findGroupChildrenByChildId("delete", children); - const deleteIndex = menuGroup?.findIndex(i => i?.props?.id === "delete"); - if (!deleteIndex || !menuGroup) return; - - menuGroup.splice(deleteIndex - 1, 0, ( - - RestAPI.patch({ - url: `/channels/${channel.id}/messages/${messageId}`, - body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED } - }) - } - /> - )); -}; - -export default definePlugin({ - name: "UnsuppressEmbeds", - authors: [Devs.rad, Devs.HypedDomi], - description: "Allows you to unsuppress embeds in messages", - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - }, -}); diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx new file mode 100644 index 0000000..a219607 --- /dev/null +++ b/src/plugins/unsuppressEmbeds/index.tsx @@ -0,0 +1,67 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { ImageInvisible, ImageVisible } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common"; + +const EMBED_SUPPRESSED = 1 << 2; + +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => { + const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0; + if (!isEmbedSuppressed && !embeds.length) return; + + const hasEmbedPerms = channel.isPrivate() || !!(PermissionStore.getChannelPermissions({ id: channel.id }) & PermissionsBits.EMBED_LINKS); + if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return; + + const menuGroup = findGroupChildrenByChildId("delete", children); + const deleteIndex = menuGroup?.findIndex(i => i?.props?.id === "delete"); + if (!deleteIndex || !menuGroup) return; + + menuGroup.splice(deleteIndex - 1, 0, ( + + RestAPI.patch({ + url: `/channels/${channel.id}/messages/${messageId}`, + body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED } + }) + } + /> + )); +}; + +export default definePlugin({ + name: "UnsuppressEmbeds", + authors: [Devs.rad, Devs.HypedDomi], + description: "Allows you to unsuppress embeds in messages", + + start() { + addContextMenuPatch("message", messageContextMenuPatch); + }, + + stop() { + removeContextMenuPatch("message", messageContextMenuPatch); + }, +}); diff --git a/src/plugins/urbanDictionary.ts b/src/plugins/urbanDictionary.ts deleted file mode 100644 index 840fe5c..0000000 --- a/src/plugins/urbanDictionary.ts +++ /dev/null @@ -1,91 +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 { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands"; -import { ApplicationCommandInputType } from "@api/Commands/types"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "UrbanDictionary", - description: "Search for a word on Urban Dictionary via /urban slash command", - authors: [Devs.jewdev], - dependencies: ["CommandsAPI"], - commands: [ - { - name: "urban", - description: "Returns the definition of a word from Urban Dictionary", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - type: ApplicationCommandOptionType.STRING, - name: "word", - description: "The word to search for on Urban Dictionary", - required: true - } - ], - 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(); - - if (!definition) - return void sendBotMessage(ctx.channel.id, { content: "No results found." }); - - const linkify = (text: string) => text - .replaceAll("\r\n", "\n") - .replace(/([*>_`~\\])/gsi, "\\$1") - .replace(/\[(.+?)\]/g, (_, word) => `[${word}](https://www.urbandictionary.com/define.php?term=${encodeURIComponent(word)} "Define '${word}' on Urban Dictionary")`) - .trim(); - - return void sendBotMessage(ctx.channel.id, { - embeds: [ - { - type: "rich", - author: { - name: `Uploaded by "${definition.author}"`, - url: `https://www.urbandictionary.com/author.php?author=${encodeURIComponent(definition.author)}`, - }, - title: definition.word, - url: `https://www.urbandictionary.com/define.php?term=${encodeURIComponent(definition.word)}`, - description: linkify(definition.definition), - fields: [ - { - name: "Example", - value: linkify(definition.example), - }, - { - name: "Want more definitions?", - value: `Check out [more definitions](https://www.urbandictionary.com/define.php?term=${query} "Define "${args[0].value}" on Urban Dictionary") on Urban Dictionary.`, - }, - ], - color: 0xFF9900, - footer: { text: `👍 ${definition.thumbs_up.toString()} | 👎 ${definition.thumbs_down.toString()}`, icon_url: "https://www.urbandictionary.com/favicon.ico" }, - timestamp: new Date(definition.written_on).toISOString(), - }, - ] as any, - }); - } catch (error) { - sendBotMessage(ctx.channel.id, { - content: `Something went wrong: \`${error}\``, - }); - } - } - } - ] -}); diff --git a/src/plugins/urbanDictionary/index.ts b/src/plugins/urbanDictionary/index.ts new file mode 100644 index 0000000..840fe5c --- /dev/null +++ b/src/plugins/urbanDictionary/index.ts @@ -0,0 +1,91 @@ +/* + * 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 { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands"; +import { ApplicationCommandInputType } from "@api/Commands/types"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "UrbanDictionary", + description: "Search for a word on Urban Dictionary via /urban slash command", + authors: [Devs.jewdev], + dependencies: ["CommandsAPI"], + commands: [ + { + name: "urban", + description: "Returns the definition of a word from Urban Dictionary", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [ + { + type: ApplicationCommandOptionType.STRING, + name: "word", + description: "The word to search for on Urban Dictionary", + required: true + } + ], + 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(); + + if (!definition) + return void sendBotMessage(ctx.channel.id, { content: "No results found." }); + + const linkify = (text: string) => text + .replaceAll("\r\n", "\n") + .replace(/([*>_`~\\])/gsi, "\\$1") + .replace(/\[(.+?)\]/g, (_, word) => `[${word}](https://www.urbandictionary.com/define.php?term=${encodeURIComponent(word)} "Define '${word}' on Urban Dictionary")`) + .trim(); + + return void sendBotMessage(ctx.channel.id, { + embeds: [ + { + type: "rich", + author: { + name: `Uploaded by "${definition.author}"`, + url: `https://www.urbandictionary.com/author.php?author=${encodeURIComponent(definition.author)}`, + }, + title: definition.word, + url: `https://www.urbandictionary.com/define.php?term=${encodeURIComponent(definition.word)}`, + description: linkify(definition.definition), + fields: [ + { + name: "Example", + value: linkify(definition.example), + }, + { + name: "Want more definitions?", + value: `Check out [more definitions](https://www.urbandictionary.com/define.php?term=${query} "Define "${args[0].value}" on Urban Dictionary") on Urban Dictionary.`, + }, + ], + color: 0xFF9900, + footer: { text: `👍 ${definition.thumbs_up.toString()} | 👎 ${definition.thumbs_down.toString()}`, icon_url: "https://www.urbandictionary.com/favicon.ico" }, + timestamp: new Date(definition.written_on).toISOString(), + }, + ] as any, + }); + } catch (error) { + sendBotMessage(ctx.channel.id, { + content: `Something went wrong: \`${error}\``, + }); + } + } + } + ] +}); diff --git a/src/plugins/validUser.tsx b/src/plugins/validUser.tsx deleted file mode 100644 index a3862d4..0000000 --- a/src/plugins/validUser.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { sleep } from "@utils/misc"; -import { Queue } from "@utils/Queue"; -import definePlugin from "@utils/types"; -import { findByCodeLazy } from "@webpack"; -import { UserStore, useState } from "@webpack/common"; -import type { User } from "discord-types/general"; -import type { ComponentType, ReactNode } from "react"; - -const fetching = new Set(); -const queue = new Queue(5); -const fetchUser = findByCodeLazy("USER(") as (id: string) => Promise; - -interface MentionProps { - data: { - userId?: string; - channelId?: string; - content: any; - }; - parse: (content: any, props: MentionProps["props"]) => ReactNode; - props: { - key: string; - formatInline: boolean; - noStyleAndInteraction: boolean; - }; - RoleMention: ComponentType; - UserMention: ComponentType; -} - -function MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) { - const [userId, setUserId] = useState(data.userId); - - // if userId is set it means the user is cached. Uncached users have userId set to undefined - if (userId) - return ( - - ); - - // Parses the raw text node array data.content into a ReactNode[]: ["<@userid>"] - const children = parse(data.content, props); - - return ( - // Discord is deranged and renders unknown user mentions as role mentions - - { - const mention = children?.[0]?.props?.children; - if (typeof mention !== "string") return; - - const id = mention.match(/<@!?(\d+)>/)?.[1]; - if (!id) return; - - if (fetching.has(id)) - return; - - if (UserStore.getUser(id)) - return setUserId(id); - - const fetch = () => { - fetching.add(id); - - queue.unshift(() => - fetchUser(id) - .then(() => { - setUserId(id); - fetching.delete(id); - }) - .catch(e => { - if (e?.status === 429) { - queue.unshift(() => sleep(1000).then(fetch)); - fetching.delete(id); - } - }) - .finally(() => sleep(300)) - ); - }; - - fetch(); - }} - > - {children} - - - ); -} - -export default definePlugin({ - name: "ValidUser", - description: "Fix mentions for unknown users showing up as '<@343383572805058560>' (hover over a mention to fix it)", - authors: [Devs.Ven], - tags: ["MentionCacheFix"], - - patches: [{ - find: 'className:"mention"', - replacement: { - // mention = { react: function (data, parse, props) { if (data.userId == null) return RoleMention() else return UserMention() - match: /react:(?=function\(\i,\i,\i\).{0,50}return null==\i\?\(0,\i\.jsx\)\((\i),.+?jsx\)\((\i),\{className:"mention")/, - // react: (...args) => OurWrapper(RoleMention, UserMention, ...args), originalReact: theirFunc - replace: "react:(...args)=>$self.renderMention($1,$2,...args),originalReact:" - } - }], - - renderMention(RoleMention, UserMention, data, parse, props) { - return ( - - - - ); - }, -}); diff --git a/src/plugins/validUser/index.tsx b/src/plugins/validUser/index.tsx new file mode 100644 index 0000000..a3862d4 --- /dev/null +++ b/src/plugins/validUser/index.tsx @@ -0,0 +1,144 @@ +/* + * 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 { Devs } from "@utils/constants"; +import { sleep } from "@utils/misc"; +import { Queue } from "@utils/Queue"; +import definePlugin from "@utils/types"; +import { findByCodeLazy } from "@webpack"; +import { UserStore, useState } from "@webpack/common"; +import type { User } from "discord-types/general"; +import type { ComponentType, ReactNode } from "react"; + +const fetching = new Set(); +const queue = new Queue(5); +const fetchUser = findByCodeLazy("USER(") as (id: string) => Promise; + +interface MentionProps { + data: { + userId?: string; + channelId?: string; + content: any; + }; + parse: (content: any, props: MentionProps["props"]) => ReactNode; + props: { + key: string; + formatInline: boolean; + noStyleAndInteraction: boolean; + }; + RoleMention: ComponentType; + UserMention: ComponentType; +} + +function MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) { + const [userId, setUserId] = useState(data.userId); + + // if userId is set it means the user is cached. Uncached users have userId set to undefined + if (userId) + return ( + + ); + + // Parses the raw text node array data.content into a ReactNode[]: ["<@userid>"] + const children = parse(data.content, props); + + return ( + // Discord is deranged and renders unknown user mentions as role mentions + + { + const mention = children?.[0]?.props?.children; + if (typeof mention !== "string") return; + + const id = mention.match(/<@!?(\d+)>/)?.[1]; + if (!id) return; + + if (fetching.has(id)) + return; + + if (UserStore.getUser(id)) + return setUserId(id); + + const fetch = () => { + fetching.add(id); + + queue.unshift(() => + fetchUser(id) + .then(() => { + setUserId(id); + fetching.delete(id); + }) + .catch(e => { + if (e?.status === 429) { + queue.unshift(() => sleep(1000).then(fetch)); + fetching.delete(id); + } + }) + .finally(() => sleep(300)) + ); + }; + + fetch(); + }} + > + {children} + + + ); +} + +export default definePlugin({ + name: "ValidUser", + description: "Fix mentions for unknown users showing up as '<@343383572805058560>' (hover over a mention to fix it)", + authors: [Devs.Ven], + tags: ["MentionCacheFix"], + + patches: [{ + find: 'className:"mention"', + replacement: { + // mention = { react: function (data, parse, props) { if (data.userId == null) return RoleMention() else return UserMention() + match: /react:(?=function\(\i,\i,\i\).{0,50}return null==\i\?\(0,\i\.jsx\)\((\i),.+?jsx\)\((\i),\{className:"mention")/, + // react: (...args) => OurWrapper(RoleMention, UserMention, ...args), originalReact: theirFunc + replace: "react:(...args)=>$self.renderMention($1,$2,...args),originalReact:" + } + }], + + renderMention(RoleMention, UserMention, data, parse, props) { + return ( + + + + ); + }, +}); diff --git a/src/plugins/vcDoubleClick.ts b/src/plugins/vcDoubleClick.ts deleted file mode 100644 index 3154351..0000000 --- a/src/plugins/vcDoubleClick.ts +++ /dev/null @@ -1,86 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { ChannelStore, SelectedChannelStore } from "@webpack/common"; - -const timers = {} as Record; - -export default definePlugin({ - name: "VoiceChatDoubleClick", - description: "Join voice chats via double click instead of single click", - authors: [Devs.Ven, Devs.D3SOX], - patches: [ - { - find: "VoiceChannel.renderPopout", - // hack: these are not React onClick, it is a custom prop handled by Discord - // thus, replacing this with onDoubleClick won't work, and you also cannot check - // e.detail since instead of the event they pass the channel. - // do this timer workaround instead - replacement: [ - // voice/stage channels - { - match: /onClick:function\(\)\{(e\.handleClick.+?)}/g, - replace: "onClick:function(){$self.schedule(()=>{$1},e)}", - }, - ], - }, - { - // channel mentions - find: ".shouldCloseDefaultModals", - replacement: { - match: /onClick:(\i)(?=,.{0,30}className:"channelMention".+?(\i)\.inContent)/, - replace: (_, onClick, props) => "" - + `onClick:(vcDoubleClickEvt)=>$self.shouldRunOnClick(vcDoubleClickEvt,${props})&&${onClick}()`, - } - } - ], - - shouldRunOnClick(e: MouseEvent, { channelId }) { - const channel = ChannelStore.getChannel(channelId); - if (!channel || ![2, 13].includes(channel.type)) return true; - return e.detail >= 2; - }, - - schedule(cb: () => void, e: any) { - const id = e.props.channel.id as string; - if (SelectedChannelStore.getVoiceChannelId() === id) { - cb(); - return; - } - // use a different counter for each channel - const data = (timers[id] ??= { timeout: void 0, i: 0 }); - // clear any existing timer - clearTimeout(data.timeout); - - // if we already have 2 or more clicks, run the callback immediately - if (++data.i >= 2) { - cb(); - delete timers[id]; - } else { - // else reset the counter in 500ms - data.timeout = setTimeout(() => { - delete timers[id]; - }, 500); - } - } -}); diff --git a/src/plugins/vcDoubleClick/index.ts b/src/plugins/vcDoubleClick/index.ts new file mode 100644 index 0000000..3154351 --- /dev/null +++ b/src/plugins/vcDoubleClick/index.ts @@ -0,0 +1,86 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { ChannelStore, SelectedChannelStore } from "@webpack/common"; + +const timers = {} as Record; + +export default definePlugin({ + name: "VoiceChatDoubleClick", + description: "Join voice chats via double click instead of single click", + authors: [Devs.Ven, Devs.D3SOX], + patches: [ + { + find: "VoiceChannel.renderPopout", + // hack: these are not React onClick, it is a custom prop handled by Discord + // thus, replacing this with onDoubleClick won't work, and you also cannot check + // e.detail since instead of the event they pass the channel. + // do this timer workaround instead + replacement: [ + // voice/stage channels + { + match: /onClick:function\(\)\{(e\.handleClick.+?)}/g, + replace: "onClick:function(){$self.schedule(()=>{$1},e)}", + }, + ], + }, + { + // channel mentions + find: ".shouldCloseDefaultModals", + replacement: { + match: /onClick:(\i)(?=,.{0,30}className:"channelMention".+?(\i)\.inContent)/, + replace: (_, onClick, props) => "" + + `onClick:(vcDoubleClickEvt)=>$self.shouldRunOnClick(vcDoubleClickEvt,${props})&&${onClick}()`, + } + } + ], + + shouldRunOnClick(e: MouseEvent, { channelId }) { + const channel = ChannelStore.getChannel(channelId); + if (!channel || ![2, 13].includes(channel.type)) return true; + return e.detail >= 2; + }, + + schedule(cb: () => void, e: any) { + const id = e.props.channel.id as string; + if (SelectedChannelStore.getVoiceChannelId() === id) { + cb(); + return; + } + // use a different counter for each channel + const data = (timers[id] ??= { timeout: void 0, i: 0 }); + // clear any existing timer + clearTimeout(data.timeout); + + // if we already have 2 or more clicks, run the callback immediately + if (++data.i >= 2) { + cb(); + delete timers[id]; + } else { + // else reset the counter in 500ms + data.timeout = setTimeout(() => { + delete timers[id]; + }, 500); + } + } +}); diff --git a/src/plugins/vcNarrator.tsx b/src/plugins/vcNarrator.tsx deleted file mode 100644 index 4447e08..0000000 --- a/src/plugins/vcNarrator.tsx +++ /dev/null @@ -1,341 +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 { Settings } from "@api/Settings"; -import { ErrorCard } from "@components/ErrorCard"; -import { Devs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import { Margins } from "@utils/margins"; -import { wordsToTitle } from "@utils/text"; -import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { Button, ChannelStore, Forms, SelectedChannelStore, useMemo, UserStore } from "@webpack/common"; - -interface VoiceState { - userId: string; - channelId?: string; - oldChannelId?: string; - deaf: boolean; - mute: boolean; - selfDeaf: boolean; - selfMute: boolean; -} - -const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId"); - -// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying -// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would -// not say the second mute, which would lead you to believe they're unmuted - -function speak(text: string, settings: any = Settings.plugins.VcNarrator) { - if (!text) return; - - const speech = new SpeechSynthesisUtterance(text); - let voice = speechSynthesis.getVoices().find(v => v.voiceURI === settings.voice); - if (!voice) { - new Logger("VcNarrator").error(`Voice "${settings.voice}" not found. Resetting to default.`); - voice = speechSynthesis.getVoices().find(v => v.default); - settings.voice = voice?.voiceURI; - if (!voice) return; // This should never happen - } - speech.voice = voice!; - speech.volume = settings.volume; - speech.rate = settings.rate; - speechSynthesis.speak(speech); -} - -function clean(str: string) { - const replacer = Settings.plugins.VcNarrator.latinOnly - ? /[^\p{Script=Latin}\p{Number}\p{Punctuation}\s]/gu - : /[^\p{Letter}\p{Number}\p{Punctuation}\s]/gu; - - return str.normalize("NFKC") - .replace(replacer, "") - .replace(/_{2,}/g, "_") - .trim(); -} - -function formatText(str: string, user: string, channel: string) { - return str - .replaceAll("{{USER}}", clean(user) || (user ? "Someone" : "")) - .replaceAll("{{CHANNEL}}", clean(channel) || "channel"); -} - -/* -let StatusMap = {} as Record; -*/ - -// For every user, channelId and oldChannelId will differ when moving channel. -// Only for the local user, channelId and oldChannelId will be the same when moving channel, -// for some ungodly reason -let myLastChannelId: string | undefined; - -function getTypeAndChannelId({ channelId, oldChannelId }: VoiceState, isMe: boolean) { - if (isMe && channelId !== myLastChannelId) { - oldChannelId = myLastChannelId; - myLastChannelId = channelId; - } - - if (channelId !== oldChannelId) { - if (channelId) return [oldChannelId ? "move" : "join", channelId]; - if (oldChannelId) return ["leave", oldChannelId]; - } - /* - if (channelId) { - if (deaf || selfDeaf) return ["deafen", channelId]; - if (mute || selfMute) return ["mute", channelId]; - const oldStatus = StatusMap[userId]; - if (oldStatus.deaf) return ["undeafen", channelId]; - if (oldStatus.mute) return ["unmute", channelId]; - } - */ - return ["", ""]; -} - -/* -function updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId, channelId }: VoiceState, isMe: boolean) { - if (isMe && (type === "join" || type === "move")) { - StatusMap = {}; - const states = VoiceStateStore.getVoiceStatesForChannel(channelId!) as Record; - for (const userId in states) { - const s = states[userId]; - StatusMap[userId] = { - mute: s.mute || s.selfMute, - deaf: s.deaf || s.selfDeaf - }; - } - return; - } - - if (type === "leave" || (type === "move" && channelId !== SelectedChannelStore.getVoiceChannelId())) { - if (isMe) - StatusMap = {}; - else - delete StatusMap[userId]; - - return; - } - - StatusMap[userId] = { - deaf: deaf || selfDeaf, - mute: mute || selfMute - }; -} -*/ - -function playSample(tempSettings: any, type: string) { - const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings); - - speak(formatText(settings[type + "Message"], UserStore.getCurrentUser().username, "general"), settings); -} - -export default definePlugin({ - name: "VcNarrator", - description: "Announces when users join, leave, or move voice channels via narrator", - authors: [Devs.Ven], - - flux: { - VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { - const myChanId = SelectedChannelStore.getVoiceChannelId(); - const myId = UserStore.getCurrentUser().id; - - if (ChannelStore.getChannel(myChanId!)?.type === 13 /* Stage Channel */) return; - - for (const state of voiceStates) { - const { userId, channelId, oldChannelId } = state; - const isMe = userId === myId; - if (!isMe) { - if (!myChanId) continue; - if (channelId !== myChanId && oldChannelId !== myChanId) continue; - } - - const [type, id] = getTypeAndChannelId(state, isMe); - if (!type) continue; - - const template = Settings.plugins.VcNarrator[type + "Message"]; - const user = isMe && !Settings.plugins.VcNarrator.sayOwnName ? "" : UserStore.getUser(userId).username; - const channel = ChannelStore.getChannel(id).name; - - speak(formatText(template, user, channel)); - - // updateStatuses(type, state, isMe); - } - }, - - AUDIO_TOGGLE_SELF_MUTE() { - const chanId = SelectedChannelStore.getVoiceChannelId()!; - const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState; - if (!s) return; - - const event = s.mute || s.selfMute ? "unmute" : "mute"; - speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name)); - }, - - AUDIO_TOGGLE_SELF_DEAF() { - const chanId = SelectedChannelStore.getVoiceChannelId()!; - const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState; - if (!s) return; - - const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen"; - speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name)); - } - }, - - start() { - if (typeof speechSynthesis === "undefined" || speechSynthesis.getVoices().length === 0) { - new Logger("VcNarrator").warn( - "SpeechSynthesis not supported or no Narrator voices found. Thus, this plugin will not work. Check my Settings for more info" - ); - return; - } - - }, - - optionsCache: null as Record | null, - - get options() { - return this.optionsCache ??= { - voice: { - type: OptionType.SELECT, - description: "Narrator Voice", - options: window.speechSynthesis?.getVoices().map(v => ({ - label: v.name, - value: v.voiceURI, - default: v.default - })) ?? [] - }, - volume: { - type: OptionType.SLIDER, - description: "Narrator Volume", - default: 1, - markers: [0, 0.25, 0.5, 0.75, 1], - stickToMarkers: false - }, - rate: { - type: OptionType.SLIDER, - description: "Narrator Speed", - default: 1, - markers: [0.1, 0.5, 1, 2, 5, 10], - stickToMarkers: false - }, - sayOwnName: { - description: "Say own name", - type: OptionType.BOOLEAN, - default: false - }, - latinOnly: { - description: "Strip non latin characters from names before saying them", - type: OptionType.BOOLEAN, - default: false - }, - joinMessage: { - type: OptionType.STRING, - description: "Join Message", - default: "{{USER}} joined" - }, - leaveMessage: { - type: OptionType.STRING, - description: "Leave Message", - default: "{{USER}} left" - }, - moveMessage: { - type: OptionType.STRING, - description: "Move Message", - default: "{{USER}} moved to {{CHANNEL}}" - }, - muteMessage: { - type: OptionType.STRING, - description: "Mute Message (only self for now)", - default: "{{USER}} Muted" - }, - unmuteMessage: { - type: OptionType.STRING, - description: "Unmute Message (only self for now)", - default: "{{USER}} unmuted" - }, - deafenMessage: { - type: OptionType.STRING, - description: "Deafen Message (only self for now)", - default: "{{USER}} deafened" - }, - undeafenMessage: { - type: OptionType.STRING, - description: "Undeafen Message (only self for now)", - default: "{{USER}} undeafened" - } - }; - }, - - settingsAboutComponent({ tempSettings: s }) { - const [hasVoices, hasEnglishVoices] = useMemo(() => { - const voices = speechSynthesis.getVoices(); - return [voices.length !== 0, voices.some(v => v.lang.startsWith("en"))]; - }, []); - - const types = useMemo( - () => Object.keys(Vencord.Plugins.plugins.VcNarrator.options!).filter(k => k.endsWith("Message")).map(k => k.slice(0, -7)), - [], - ); - - let errorComponent: React.ReactElement | null = null; - if (!hasVoices) { - let error = "No narrator voices found. "; - error += navigator.platform?.toLowerCase().includes("linux") - ? "Install speech-dispatcher or espeak and run Discord with the --enable-speech-dispatcher flag" - : "Try installing some in the Narrator settings of your Operating System"; - errorComponent = {error}; - } else if (!hasEnglishVoices) { - errorComponent = You don't have any English voices installed, so the narrator might sound weird; - } - - return ( - - - You can customise the spoken messages below. You can disable specific messages by setting them to nothing - - - The special placeholders {"{{USER}}"} and {"{{CHANNEL}}"}{" "} - will be replaced with the user's name (nothing if it's yourself) and the channel's name respectively - - {hasEnglishVoices && ( - <> - Play Example Sounds -
- {types.map(t => ( - - ))} -
- - )} - {errorComponent} -
- ); - } -}); diff --git a/src/plugins/vcNarrator/index.tsx b/src/plugins/vcNarrator/index.tsx new file mode 100644 index 0000000..4447e08 --- /dev/null +++ b/src/plugins/vcNarrator/index.tsx @@ -0,0 +1,341 @@ +/* + * 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 { Settings } from "@api/Settings"; +import { ErrorCard } from "@components/ErrorCard"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import { Margins } from "@utils/margins"; +import { wordsToTitle } from "@utils/text"; +import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Button, ChannelStore, Forms, SelectedChannelStore, useMemo, UserStore } from "@webpack/common"; + +interface VoiceState { + userId: string; + channelId?: string; + oldChannelId?: string; + deaf: boolean; + mute: boolean; + selfDeaf: boolean; + selfMute: boolean; +} + +const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId"); + +// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying +// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would +// not say the second mute, which would lead you to believe they're unmuted + +function speak(text: string, settings: any = Settings.plugins.VcNarrator) { + if (!text) return; + + const speech = new SpeechSynthesisUtterance(text); + let voice = speechSynthesis.getVoices().find(v => v.voiceURI === settings.voice); + if (!voice) { + new Logger("VcNarrator").error(`Voice "${settings.voice}" not found. Resetting to default.`); + voice = speechSynthesis.getVoices().find(v => v.default); + settings.voice = voice?.voiceURI; + if (!voice) return; // This should never happen + } + speech.voice = voice!; + speech.volume = settings.volume; + speech.rate = settings.rate; + speechSynthesis.speak(speech); +} + +function clean(str: string) { + const replacer = Settings.plugins.VcNarrator.latinOnly + ? /[^\p{Script=Latin}\p{Number}\p{Punctuation}\s]/gu + : /[^\p{Letter}\p{Number}\p{Punctuation}\s]/gu; + + return str.normalize("NFKC") + .replace(replacer, "") + .replace(/_{2,}/g, "_") + .trim(); +} + +function formatText(str: string, user: string, channel: string) { + return str + .replaceAll("{{USER}}", clean(user) || (user ? "Someone" : "")) + .replaceAll("{{CHANNEL}}", clean(channel) || "channel"); +} + +/* +let StatusMap = {} as Record; +*/ + +// For every user, channelId and oldChannelId will differ when moving channel. +// Only for the local user, channelId and oldChannelId will be the same when moving channel, +// for some ungodly reason +let myLastChannelId: string | undefined; + +function getTypeAndChannelId({ channelId, oldChannelId }: VoiceState, isMe: boolean) { + if (isMe && channelId !== myLastChannelId) { + oldChannelId = myLastChannelId; + myLastChannelId = channelId; + } + + if (channelId !== oldChannelId) { + if (channelId) return [oldChannelId ? "move" : "join", channelId]; + if (oldChannelId) return ["leave", oldChannelId]; + } + /* + if (channelId) { + if (deaf || selfDeaf) return ["deafen", channelId]; + if (mute || selfMute) return ["mute", channelId]; + const oldStatus = StatusMap[userId]; + if (oldStatus.deaf) return ["undeafen", channelId]; + if (oldStatus.mute) return ["unmute", channelId]; + } + */ + return ["", ""]; +} + +/* +function updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId, channelId }: VoiceState, isMe: boolean) { + if (isMe && (type === "join" || type === "move")) { + StatusMap = {}; + const states = VoiceStateStore.getVoiceStatesForChannel(channelId!) as Record; + for (const userId in states) { + const s = states[userId]; + StatusMap[userId] = { + mute: s.mute || s.selfMute, + deaf: s.deaf || s.selfDeaf + }; + } + return; + } + + if (type === "leave" || (type === "move" && channelId !== SelectedChannelStore.getVoiceChannelId())) { + if (isMe) + StatusMap = {}; + else + delete StatusMap[userId]; + + return; + } + + StatusMap[userId] = { + deaf: deaf || selfDeaf, + mute: mute || selfMute + }; +} +*/ + +function playSample(tempSettings: any, type: string) { + const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings); + + speak(formatText(settings[type + "Message"], UserStore.getCurrentUser().username, "general"), settings); +} + +export default definePlugin({ + name: "VcNarrator", + description: "Announces when users join, leave, or move voice channels via narrator", + authors: [Devs.Ven], + + flux: { + VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { + const myChanId = SelectedChannelStore.getVoiceChannelId(); + const myId = UserStore.getCurrentUser().id; + + if (ChannelStore.getChannel(myChanId!)?.type === 13 /* Stage Channel */) return; + + for (const state of voiceStates) { + const { userId, channelId, oldChannelId } = state; + const isMe = userId === myId; + if (!isMe) { + if (!myChanId) continue; + if (channelId !== myChanId && oldChannelId !== myChanId) continue; + } + + const [type, id] = getTypeAndChannelId(state, isMe); + if (!type) continue; + + const template = Settings.plugins.VcNarrator[type + "Message"]; + const user = isMe && !Settings.plugins.VcNarrator.sayOwnName ? "" : UserStore.getUser(userId).username; + const channel = ChannelStore.getChannel(id).name; + + speak(formatText(template, user, channel)); + + // updateStatuses(type, state, isMe); + } + }, + + AUDIO_TOGGLE_SELF_MUTE() { + const chanId = SelectedChannelStore.getVoiceChannelId()!; + const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState; + if (!s) return; + + const event = s.mute || s.selfMute ? "unmute" : "mute"; + speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name)); + }, + + AUDIO_TOGGLE_SELF_DEAF() { + const chanId = SelectedChannelStore.getVoiceChannelId()!; + const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState; + if (!s) return; + + const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen"; + speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name)); + } + }, + + start() { + if (typeof speechSynthesis === "undefined" || speechSynthesis.getVoices().length === 0) { + new Logger("VcNarrator").warn( + "SpeechSynthesis not supported or no Narrator voices found. Thus, this plugin will not work. Check my Settings for more info" + ); + return; + } + + }, + + optionsCache: null as Record | null, + + get options() { + return this.optionsCache ??= { + voice: { + type: OptionType.SELECT, + description: "Narrator Voice", + options: window.speechSynthesis?.getVoices().map(v => ({ + label: v.name, + value: v.voiceURI, + default: v.default + })) ?? [] + }, + volume: { + type: OptionType.SLIDER, + description: "Narrator Volume", + default: 1, + markers: [0, 0.25, 0.5, 0.75, 1], + stickToMarkers: false + }, + rate: { + type: OptionType.SLIDER, + description: "Narrator Speed", + default: 1, + markers: [0.1, 0.5, 1, 2, 5, 10], + stickToMarkers: false + }, + sayOwnName: { + description: "Say own name", + type: OptionType.BOOLEAN, + default: false + }, + latinOnly: { + description: "Strip non latin characters from names before saying them", + type: OptionType.BOOLEAN, + default: false + }, + joinMessage: { + type: OptionType.STRING, + description: "Join Message", + default: "{{USER}} joined" + }, + leaveMessage: { + type: OptionType.STRING, + description: "Leave Message", + default: "{{USER}} left" + }, + moveMessage: { + type: OptionType.STRING, + description: "Move Message", + default: "{{USER}} moved to {{CHANNEL}}" + }, + muteMessage: { + type: OptionType.STRING, + description: "Mute Message (only self for now)", + default: "{{USER}} Muted" + }, + unmuteMessage: { + type: OptionType.STRING, + description: "Unmute Message (only self for now)", + default: "{{USER}} unmuted" + }, + deafenMessage: { + type: OptionType.STRING, + description: "Deafen Message (only self for now)", + default: "{{USER}} deafened" + }, + undeafenMessage: { + type: OptionType.STRING, + description: "Undeafen Message (only self for now)", + default: "{{USER}} undeafened" + } + }; + }, + + settingsAboutComponent({ tempSettings: s }) { + const [hasVoices, hasEnglishVoices] = useMemo(() => { + const voices = speechSynthesis.getVoices(); + return [voices.length !== 0, voices.some(v => v.lang.startsWith("en"))]; + }, []); + + const types = useMemo( + () => Object.keys(Vencord.Plugins.plugins.VcNarrator.options!).filter(k => k.endsWith("Message")).map(k => k.slice(0, -7)), + [], + ); + + let errorComponent: React.ReactElement | null = null; + if (!hasVoices) { + let error = "No narrator voices found. "; + error += navigator.platform?.toLowerCase().includes("linux") + ? "Install speech-dispatcher or espeak and run Discord with the --enable-speech-dispatcher flag" + : "Try installing some in the Narrator settings of your Operating System"; + errorComponent = {error}; + } else if (!hasEnglishVoices) { + errorComponent = You don't have any English voices installed, so the narrator might sound weird; + } + + return ( + + + You can customise the spoken messages below. You can disable specific messages by setting them to nothing + + + The special placeholders {"{{USER}}"} and {"{{CHANNEL}}"}{" "} + will be replaced with the user's name (nothing if it's yourself) and the channel's name respectively + + {hasEnglishVoices && ( + <> + Play Example Sounds +
+ {types.map(t => ( + + ))} +
+ + )} + {errorComponent} +
+ ); + } +}); diff --git a/src/plugins/viewIcons.tsx b/src/plugins/viewIcons.tsx deleted file mode 100644 index 3bfe902..0000000 --- a/src/plugins/viewIcons.tsx +++ /dev/null @@ -1,200 +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 { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; -import { definePluginSettings } from "@api/Settings"; -import { ImageIcon } from "@components/Icons"; -import { Devs } from "@utils/constants"; -import { openImageModal } from "@utils/discord"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { GuildMemberStore, Menu } from "@webpack/common"; -import type { Channel, Guild, User } from "discord-types/general"; - -const BannerStore = findByPropsLazy("getGuildBannerURL"); - -interface UserContextProps { - channel: Channel; - guildId?: string; - user: User; -} - -interface GuildContextProps { - guild?: Guild; -} - -const settings = definePluginSettings({ - format: { - type: OptionType.SELECT, - description: "Choose the image format to use for non animated images. Animated images will always use .gif", - options: [ - { - label: "webp", - value: "webp", - default: true - }, - { - label: "png", - value: "png", - }, - { - label: "jpg", - value: "jpg", - } - ] - }, - imgSize: { - type: OptionType.SELECT, - description: "The image size to use", - options: ["128", "256", "512", "1024", "2048", "4096"].map(n => ({ label: n, value: n, default: n === "1024" })) - } -}); - -function openImage(url: string) { - const format = url.startsWith("/") ? "png" : settings.store.format; - - const u = new URL(url, window.location.href); - u.searchParams.set("size", settings.store.imgSize); - u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`); - url = u.toString(); - - u.searchParams.set("size", "4096"); - const originalUrl = u.toString(); - - openImageModal(url, { - original: originalUrl, - height: 256 - }); -} - -const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => { - const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null; - - children.splice(-1, 0, ( - - openImage(BannerStore.getUserAvatarURL(user, true))} - icon={ImageIcon} - /> - {memberAvatar && ( - openImage(BannerStore.getGuildMemberAvatarURLSimple({ - userId: user.id, - avatar: memberAvatar, - guildId, - canAnimate: true - }, true))} - icon={ImageIcon} - /> - )} - - )); -}; - -const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => { - if(!guild) return; - - const { id, icon, banner } = guild; - if (!banner && !icon) return; - - children.splice(-1, 0, ( - - {icon ? ( - - openImage(BannerStore.getGuildIconURL({ - id, - icon, - canAnimate: true - })) - } - icon={ImageIcon} - /> - ) : null} - {banner ? ( - - openImage(BannerStore.getGuildBannerURL({ - id, - banner, - }, true)) - } - icon={ImageIcon} - /> - ) : null} - - )); -}; - -export default definePlugin({ - name: "ViewIcons", - authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz], - description: "Makes avatars and banners in user profiles clickable, and adds View Icon/Banner entries in the user and server context menu", - tags: ["ImageUtilities"], - - settings, - - openImage, - - start() { - addContextMenuPatch("user-context", UserContext); - addContextMenuPatch("guild-context", GuildContext); - }, - - stop() { - removeContextMenuPatch("user-context", UserContext); - removeContextMenuPatch("guild-context", GuildContext); - }, - - patches: [ - // Make pfps clickable - { - find: "onAddFriend:", - replacement: { - match: /\{src:(\i)(?=,avatarDecoration)/, - replace: "{src:$1,onClick:()=>$self.openImage($1)" - } - }, - // Make banners clickable - { - find: ".NITRO_BANNER,", - replacement: { - // style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl, - match: /style:\{(?=backgroundImage:(\i&&\i)\?"url\("\.concat\((\i),)/, - replace: - // onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0, - 'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,' - } - }, - { - find: "().avatarWrapperNonUserBot", - replacement: { - match: /(?<=avatarPositionPanel.+?)onClick:(\i\|\|\i)\?void 0(?<=,(\i)=\i\.avatarSrc.+?)/, - replace: "style:($1)?{cursor:\"pointer\"}:{},onClick:$1?()=>{$self.openImage($2)}" - } - } - ] -}); diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx new file mode 100644 index 0000000..3bfe902 --- /dev/null +++ b/src/plugins/viewIcons/index.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 { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { definePluginSettings } from "@api/Settings"; +import { ImageIcon } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import { openImageModal } from "@utils/discord"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { GuildMemberStore, Menu } from "@webpack/common"; +import type { Channel, Guild, User } from "discord-types/general"; + +const BannerStore = findByPropsLazy("getGuildBannerURL"); + +interface UserContextProps { + channel: Channel; + guildId?: string; + user: User; +} + +interface GuildContextProps { + guild?: Guild; +} + +const settings = definePluginSettings({ + format: { + type: OptionType.SELECT, + description: "Choose the image format to use for non animated images. Animated images will always use .gif", + options: [ + { + label: "webp", + value: "webp", + default: true + }, + { + label: "png", + value: "png", + }, + { + label: "jpg", + value: "jpg", + } + ] + }, + imgSize: { + type: OptionType.SELECT, + description: "The image size to use", + options: ["128", "256", "512", "1024", "2048", "4096"].map(n => ({ label: n, value: n, default: n === "1024" })) + } +}); + +function openImage(url: string) { + const format = url.startsWith("/") ? "png" : settings.store.format; + + const u = new URL(url, window.location.href); + u.searchParams.set("size", settings.store.imgSize); + u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`); + url = u.toString(); + + u.searchParams.set("size", "4096"); + const originalUrl = u.toString(); + + openImageModal(url, { + original: originalUrl, + height: 256 + }); +} + +const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => { + const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null; + + children.splice(-1, 0, ( + + openImage(BannerStore.getUserAvatarURL(user, true))} + icon={ImageIcon} + /> + {memberAvatar && ( + openImage(BannerStore.getGuildMemberAvatarURLSimple({ + userId: user.id, + avatar: memberAvatar, + guildId, + canAnimate: true + }, true))} + icon={ImageIcon} + /> + )} + + )); +}; + +const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => { + if(!guild) return; + + const { id, icon, banner } = guild; + if (!banner && !icon) return; + + children.splice(-1, 0, ( + + {icon ? ( + + openImage(BannerStore.getGuildIconURL({ + id, + icon, + canAnimate: true + })) + } + icon={ImageIcon} + /> + ) : null} + {banner ? ( + + openImage(BannerStore.getGuildBannerURL({ + id, + banner, + }, true)) + } + icon={ImageIcon} + /> + ) : null} + + )); +}; + +export default definePlugin({ + name: "ViewIcons", + authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz], + description: "Makes avatars and banners in user profiles clickable, and adds View Icon/Banner entries in the user and server context menu", + tags: ["ImageUtilities"], + + settings, + + openImage, + + start() { + addContextMenuPatch("user-context", UserContext); + addContextMenuPatch("guild-context", GuildContext); + }, + + stop() { + removeContextMenuPatch("user-context", UserContext); + removeContextMenuPatch("guild-context", GuildContext); + }, + + patches: [ + // Make pfps clickable + { + find: "onAddFriend:", + replacement: { + match: /\{src:(\i)(?=,avatarDecoration)/, + replace: "{src:$1,onClick:()=>$self.openImage($1)" + } + }, + // Make banners clickable + { + find: ".NITRO_BANNER,", + replacement: { + // style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl, + match: /style:\{(?=backgroundImage:(\i&&\i)\?"url\("\.concat\((\i),)/, + replace: + // onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0, + 'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,' + } + }, + { + find: "().avatarWrapperNonUserBot", + replacement: { + match: /(?<=avatarPositionPanel.+?)onClick:(\i\|\|\i)\?void 0(?<=,(\i)=\i\.avatarSrc.+?)/, + replace: "style:($1)?{cursor:\"pointer\"}:{},onClick:$1?()=>{$self.openImage($2)}" + } + } + ] +}); diff --git a/src/plugins/viewRaw.tsx b/src/plugins/viewRaw.tsx deleted file mode 100644 index 6012764..0000000 --- a/src/plugins/viewRaw.tsx +++ /dev/null @@ -1,198 +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 { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; -import { addButton, removeButton } from "@api/MessagePopover"; -import { definePluginSettings } from "@api/Settings"; -import { CodeBlock } from "@components/CodeBlock"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Flex } from "@components/Flex"; -import { Devs } from "@utils/constants"; -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 { Message } from "discord-types/general"; - - -const CopyIcon = () => { - return ; -}; - -function sortObject(obj: T): T { - return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T; -} - -function cleanMessage(msg: Message) { - const clone = sortObject(JSON.parse(JSON.stringify(msg))); - for (const key of [ - "email", - "phone", - "mfaEnabled", - "personalConnectionId" - ]) delete clone.author[key]; - - // message logger added properties - const cloneAny = clone as any; - delete cloneAny.editHistory; - delete cloneAny.deleted; - cloneAny.attachments?.forEach(a => delete a.deleted); - - return clone; -} - -function openViewRawModal(json: string, type: string, msgContent?: string) { - const key = openModal(props => ( - - - - View Raw - closeModal(key)} /> - - -
- {!!msgContent && ( - <> - Content - - - - )} - - {type} Data - -
-
- - - - {!!msgContent && ( - - )} - - -
-
- )); -} - -function openViewRawModalMessage(msg: Message) { - msg = cleanMessage(msg); - const msgJson = JSON.stringify(msg, null, 4); - - return openViewRawModal(msgJson, "Message", msg.content); -} - -const settings = definePluginSettings({ - clickMethod: { - description: "Change the button to view the raw content/data of any message.", - type: OptionType.SELECT, - options: [ - { label: "Left Click to view the raw content.", value: "Left", default: true }, - { label: "Right click to view the raw content.", value: "Right" } - ] - } -}); - -function MakeContextCallback(name: string) { - const callback: NavContextMenuPatchCallback = (children, props) => () => { - if (name === "Guild" && !props.guild) return; - 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.splice(-1, 0, - openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)} - icon={CopyIcon} - /> - ); - }; - 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, - - start() { - addButton("ViewRaw", msg => { - const handleClick = () => { - if (settings.store.clickMethod === "Right") { - copyWithToast(msg.content); - } else { - openViewRawModalMessage(msg); - } - }; - - const handleContextMenu = e => { - if (settings.store.clickMethod === "Left") { - e.preventDefault(); - e.stopPropagation(); - copyWithToast(msg.content); - } else { - e.preventDefault(); - e.stopPropagation(); - openViewRawModalMessage(msg); - } - }; - - const label = settings.store.clickMethod === "Right" - ? "Copy Raw (Left Click) / View Raw (Right Click)" - : "View Raw (Left Click) / Copy Raw (Right Click)"; - - return { - label, - icon: CopyIcon, - message: msg, - channel: ChannelStore.getChannel(msg.channel_id), - onClick: handleClick, - onContextMenu: handleContextMenu - }; - }); - - 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")); - } -}); diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx new file mode 100644 index 0000000..6012764 --- /dev/null +++ b/src/plugins/viewRaw/index.tsx @@ -0,0 +1,198 @@ +/* + * 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 { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { addButton, removeButton } from "@api/MessagePopover"; +import { definePluginSettings } from "@api/Settings"; +import { CodeBlock } from "@components/CodeBlock"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Flex } from "@components/Flex"; +import { Devs } from "@utils/constants"; +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 { Message } from "discord-types/general"; + + +const CopyIcon = () => { + return ; +}; + +function sortObject(obj: T): T { + return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T; +} + +function cleanMessage(msg: Message) { + const clone = sortObject(JSON.parse(JSON.stringify(msg))); + for (const key of [ + "email", + "phone", + "mfaEnabled", + "personalConnectionId" + ]) delete clone.author[key]; + + // message logger added properties + const cloneAny = clone as any; + delete cloneAny.editHistory; + delete cloneAny.deleted; + cloneAny.attachments?.forEach(a => delete a.deleted); + + return clone; +} + +function openViewRawModal(json: string, type: string, msgContent?: string) { + const key = openModal(props => ( + + + + View Raw + closeModal(key)} /> + + +
+ {!!msgContent && ( + <> + Content + + + + )} + + {type} Data + +
+
+ + + + {!!msgContent && ( + + )} + + +
+
+ )); +} + +function openViewRawModalMessage(msg: Message) { + msg = cleanMessage(msg); + const msgJson = JSON.stringify(msg, null, 4); + + return openViewRawModal(msgJson, "Message", msg.content); +} + +const settings = definePluginSettings({ + clickMethod: { + description: "Change the button to view the raw content/data of any message.", + type: OptionType.SELECT, + options: [ + { label: "Left Click to view the raw content.", value: "Left", default: true }, + { label: "Right click to view the raw content.", value: "Right" } + ] + } +}); + +function MakeContextCallback(name: string) { + const callback: NavContextMenuPatchCallback = (children, props) => () => { + if (name === "Guild" && !props.guild) return; + 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.splice(-1, 0, + openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)} + icon={CopyIcon} + /> + ); + }; + 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, + + start() { + addButton("ViewRaw", msg => { + const handleClick = () => { + if (settings.store.clickMethod === "Right") { + copyWithToast(msg.content); + } else { + openViewRawModalMessage(msg); + } + }; + + const handleContextMenu = e => { + if (settings.store.clickMethod === "Left") { + e.preventDefault(); + e.stopPropagation(); + copyWithToast(msg.content); + } else { + e.preventDefault(); + e.stopPropagation(); + openViewRawModalMessage(msg); + } + }; + + const label = settings.store.clickMethod === "Right" + ? "Copy Raw (Left Click) / View Raw (Right Click)" + : "View Raw (Left Click) / Copy Raw (Right Click)"; + + return { + label, + icon: CopyIcon, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: handleClick, + onContextMenu: handleContextMenu + }; + }); + + 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")); + } +}); diff --git a/src/plugins/volumeBooster.discordDesktop.ts b/src/plugins/volumeBooster.discordDesktop.ts deleted file mode 100644 index 4aca79f..0000000 --- a/src/plugins/volumeBooster.discordDesktop.ts +++ /dev/null @@ -1,86 +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 { definePluginSettings } from "@api/Settings"; -import { makeRange } from "@components/PluginSettings/components"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const settings = definePluginSettings({ - multiplier: { - description: "Volume Multiplier", - type: OptionType.SLIDER, - markers: makeRange(1, 5, 1), - default: 2, - stickToMarkers: true, - } -}); - -export default definePlugin({ - name: "VolumeBooster", - authors: [Devs.Nuckyz], - description: "Allows you to set the user and stream volume above the default maximum.", - settings, - - patches: [ - // Change the max volume for sliders to allow for values above 200 - ...[ - ".Messages.USER_VOLUME", - "currentVolume:" - ].map(find => ({ - find, - replacement: { - match: /(?<=maxValue:\i\.\i)\?(\d+?):(\d+?)(?=,)/, - replace: (_, higherMaxVolume, minorMaxVolume) => "" - + `?${higherMaxVolume}*$self.settings.store.multiplier` - + `:${minorMaxVolume}*$self.settings.store.multiplier` - } - })), - // Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord - { - find: "AudioContextSettingsMigrated", - replacement: [ - { - match: /(?<=isLocalMute\(\i,\i\),volume:.+?volume:)\i(?=})/, - replace: "$&>200?200:$&" - }, - { - match: /(?<=Object\.entries\(\i\.localMutes\).+?volume:).+?(?=,)/, - replace: "$&>200?200:$&" - }, - { - match: /(?<=Object\.entries\(\i\.localVolumes\).+?volume:).+?(?=})/, - replace: "$&>200?200:$&" - } - ] - }, - // Prevent the MediaEngineStore from overwriting our LocalVolumes above 200 with the ones the Discord Audio Context Settings sync sends - { - find: '.displayName="MediaEngineStore"', - replacement: [ - { - match: /(\.settings\.audioContextSettings.+?)(\i\[\i\])=(\i\.volume)(.+?setLocalVolume\(\i,).+?\)/, - replace: (_, rest1, localVolume, syncVolume, rest2) => rest1 - + `(${localVolume}>200?void 0:${localVolume}=${syncVolume})` - + rest2 - + `${localVolume}??${syncVolume})` - } - ] - } - ], -}); diff --git a/src/plugins/volumeBooster.discordDesktop/index.ts b/src/plugins/volumeBooster.discordDesktop/index.ts new file mode 100644 index 0000000..4aca79f --- /dev/null +++ b/src/plugins/volumeBooster.discordDesktop/index.ts @@ -0,0 +1,86 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { makeRange } from "@components/PluginSettings/components"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +const settings = definePluginSettings({ + multiplier: { + description: "Volume Multiplier", + type: OptionType.SLIDER, + markers: makeRange(1, 5, 1), + default: 2, + stickToMarkers: true, + } +}); + +export default definePlugin({ + name: "VolumeBooster", + authors: [Devs.Nuckyz], + description: "Allows you to set the user and stream volume above the default maximum.", + settings, + + patches: [ + // Change the max volume for sliders to allow for values above 200 + ...[ + ".Messages.USER_VOLUME", + "currentVolume:" + ].map(find => ({ + find, + replacement: { + match: /(?<=maxValue:\i\.\i)\?(\d+?):(\d+?)(?=,)/, + replace: (_, higherMaxVolume, minorMaxVolume) => "" + + `?${higherMaxVolume}*$self.settings.store.multiplier` + + `:${minorMaxVolume}*$self.settings.store.multiplier` + } + })), + // Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord + { + find: "AudioContextSettingsMigrated", + replacement: [ + { + match: /(?<=isLocalMute\(\i,\i\),volume:.+?volume:)\i(?=})/, + replace: "$&>200?200:$&" + }, + { + match: /(?<=Object\.entries\(\i\.localMutes\).+?volume:).+?(?=,)/, + replace: "$&>200?200:$&" + }, + { + match: /(?<=Object\.entries\(\i\.localVolumes\).+?volume:).+?(?=})/, + replace: "$&>200?200:$&" + } + ] + }, + // Prevent the MediaEngineStore from overwriting our LocalVolumes above 200 with the ones the Discord Audio Context Settings sync sends + { + find: '.displayName="MediaEngineStore"', + replacement: [ + { + match: /(\.settings\.audioContextSettings.+?)(\i\[\i\])=(\i\.volume)(.+?setLocalVolume\(\i,).+?\)/, + replace: (_, rest1, localVolume, syncVolume, rest2) => rest1 + + `(${localVolume}>200?void 0:${localVolume}=${syncVolume})` + + rest2 + + `${localVolume}??${syncVolume})` + } + ] + } + ], +}); diff --git a/src/plugins/webContextMenus.web.ts b/src/plugins/webContextMenus.web.ts deleted file mode 100644 index 4cdbcd9..0000000 --- a/src/plugins/webContextMenus.web.ts +++ /dev/null @@ -1,232 +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 { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { saveFile } from "@utils/web"; -import { findByProps, findLazy } from "@webpack"; -import { Clipboard } from "@webpack/common"; - -async function fetchImage(url: string) { - const res = await fetch(url); - if (res.status !== 200) return; - - return await res.blob(); -} - -const MiniDispatcher = findLazy(m => m.emitter?._events?.INSERT_TEXT); - -const settings = definePluginSettings({ - // This needs to be all in one setting because to enable any of these, we need to make Discord use their desktop context - // menu handler instead of the web one, which breaks the other menus that aren't enabled - addBack: { - type: OptionType.BOOLEAN, - description: "Add back the Discord context menus for images, links and the chat input bar", - // Web slate menu has proper spellcheck suggestions and image context menu is also pretty good, - // so disable this by default. Vesktop just doesn't, so enable by default - default: IS_VESKTOP, - restartNeeded: true - } -}); - -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)", - authors: [Devs.Ven], - enabledByDefault: true, - required: IS_VESKTOP, - - settings, - - start() { - if (settings.store.addBack) { - const ctxMenuCallbacks = findByProps("contextMenuCallbackNative"); - window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb); - window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative); - this.changedListeners = true; - } - }, - - stop() { - if (this.changedListeners) { - const ctxMenuCallbacks = findByProps("contextMenuCallbackNative"); - window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative); - window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb); - } - }, - - patches: [ - // Add back Copy & Open Link - { - // There is literally no reason for Discord to make this Desktop only. - // The only thing broken is copy, but they already have a different copy function - // with web support???? - find: "open-native-link", - replacement: [ - { - // if (IS_DESKTOP || null == ...) - match: /if\(!\i\.\i\|\|null==/, - replace: "if(null==" - }, - // Fix silly Discord calling the non web support copy - { - match: /\i\.\i\.copy/, - replace: "Vencord.Webpack.Common.Clipboard.copy" - } - ] - }, - - // Add back Copy & Save Image - { - find: 'id:"copy-image"', - replacement: [ - { - // if (!IS_WEB || null == - match: /if\(!\i\.\i\|\|null==/, - replace: "if(null==" - }, - { - match: /return\s*?\[\i\.\i\.canCopyImage\(\)/, - replace: "return [true" - }, - { - match: /(?<=COPY_IMAGE_MENU_ITEM,)action:/, - replace: "action:()=>$self.copyImage(arguments[0]),oldAction:" - }, - { - match: /(?<=SAVE_IMAGE_MENU_ITEM,)action:/, - replace: "action:()=>$self.saveImage(arguments[0]),oldAction:" - }, - ] - }, - - // Add back image context menu - { - find: 'navId:"image-context"', - predicate: () => settings.store.addBack, - replacement: { - // return IS_DESKTOP ? React.createElement(Menu, ...) - match: /return \i\.\i\?/, - replace: "return true?" - } - }, - - // Add back link context menu - { - find: '"interactionUsernameProfile"', - predicate: () => settings.store.addBack, - replacement: { - match: /if\("A"===\i\.tagName&&""!==\i\.textContent\)/, - replace: "if(false)" - } - }, - - // Add back slate / text input context menu - { - find: '"slate-toolbar"', - predicate: () => settings.store.addBack, - replacement: { - match: /(?<=\.handleContextMenu=.+?"bottom";)\i\.\i\?/, - replace: "true?" - } - }, - { - find: 'navId:"textarea-context"', - all: true, - predicate: () => settings.store.addBack, - replacement: [ - { - // if (!IS_DESKTOP) return null; - match: /if\(!\i\.\i\)return null;/, - replace: "" - }, - { - // Change calls to DiscordNative.clipboard to us instead - match: /\b\i\.\i\.(copy|cut|paste)/g, - replace: "$self.$1" - } - ] - }, - { - find: '"add-to-dictionary"', - predicate: () => settings.store.addBack, - replacement: { - match: /var \i=\i\.text,/, - replace: "return [null,null];$&" - } - } - ], - - async copyImage(url: string) { - // 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; - }, - - async saveImage(url: string) { - const data = await fetchImage(url); - if (!data) return; - - const name = new URL(url).pathname.split("/").pop()!; - const file = new File([data], name, { type: data.type }); - - saveFile(file); - }, - - copy() { - const selection = document.getSelection(); - if (!selection) return; - - Clipboard.copy(selection.toString()); - }, - - cut() { - this.copy(); - MiniDispatcher.dispatch("INSERT_TEXT", { rawText: "" }); - }, - - async paste() { - const text = await navigator.clipboard.readText(); - - const data = new DataTransfer(); - data.setData("text/plain", text); - - document.dispatchEvent( - new ClipboardEvent("paste", { - clipboardData: data - }) - ); - } -}); diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts new file mode 100644 index 0000000..4cdbcd9 --- /dev/null +++ b/src/plugins/webContextMenus.web/index.ts @@ -0,0 +1,232 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { saveFile } from "@utils/web"; +import { findByProps, findLazy } from "@webpack"; +import { Clipboard } from "@webpack/common"; + +async function fetchImage(url: string) { + const res = await fetch(url); + if (res.status !== 200) return; + + return await res.blob(); +} + +const MiniDispatcher = findLazy(m => m.emitter?._events?.INSERT_TEXT); + +const settings = definePluginSettings({ + // This needs to be all in one setting because to enable any of these, we need to make Discord use their desktop context + // menu handler instead of the web one, which breaks the other menus that aren't enabled + addBack: { + type: OptionType.BOOLEAN, + description: "Add back the Discord context menus for images, links and the chat input bar", + // Web slate menu has proper spellcheck suggestions and image context menu is also pretty good, + // so disable this by default. Vesktop just doesn't, so enable by default + default: IS_VESKTOP, + restartNeeded: true + } +}); + +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)", + authors: [Devs.Ven], + enabledByDefault: true, + required: IS_VESKTOP, + + settings, + + start() { + if (settings.store.addBack) { + const ctxMenuCallbacks = findByProps("contextMenuCallbackNative"); + window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb); + window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative); + this.changedListeners = true; + } + }, + + stop() { + if (this.changedListeners) { + const ctxMenuCallbacks = findByProps("contextMenuCallbackNative"); + window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative); + window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb); + } + }, + + patches: [ + // Add back Copy & Open Link + { + // There is literally no reason for Discord to make this Desktop only. + // The only thing broken is copy, but they already have a different copy function + // with web support???? + find: "open-native-link", + replacement: [ + { + // if (IS_DESKTOP || null == ...) + match: /if\(!\i\.\i\|\|null==/, + replace: "if(null==" + }, + // Fix silly Discord calling the non web support copy + { + match: /\i\.\i\.copy/, + replace: "Vencord.Webpack.Common.Clipboard.copy" + } + ] + }, + + // Add back Copy & Save Image + { + find: 'id:"copy-image"', + replacement: [ + { + // if (!IS_WEB || null == + match: /if\(!\i\.\i\|\|null==/, + replace: "if(null==" + }, + { + match: /return\s*?\[\i\.\i\.canCopyImage\(\)/, + replace: "return [true" + }, + { + match: /(?<=COPY_IMAGE_MENU_ITEM,)action:/, + replace: "action:()=>$self.copyImage(arguments[0]),oldAction:" + }, + { + match: /(?<=SAVE_IMAGE_MENU_ITEM,)action:/, + replace: "action:()=>$self.saveImage(arguments[0]),oldAction:" + }, + ] + }, + + // Add back image context menu + { + find: 'navId:"image-context"', + predicate: () => settings.store.addBack, + replacement: { + // return IS_DESKTOP ? React.createElement(Menu, ...) + match: /return \i\.\i\?/, + replace: "return true?" + } + }, + + // Add back link context menu + { + find: '"interactionUsernameProfile"', + predicate: () => settings.store.addBack, + replacement: { + match: /if\("A"===\i\.tagName&&""!==\i\.textContent\)/, + replace: "if(false)" + } + }, + + // Add back slate / text input context menu + { + find: '"slate-toolbar"', + predicate: () => settings.store.addBack, + replacement: { + match: /(?<=\.handleContextMenu=.+?"bottom";)\i\.\i\?/, + replace: "true?" + } + }, + { + find: 'navId:"textarea-context"', + all: true, + predicate: () => settings.store.addBack, + replacement: [ + { + // if (!IS_DESKTOP) return null; + match: /if\(!\i\.\i\)return null;/, + replace: "" + }, + { + // Change calls to DiscordNative.clipboard to us instead + match: /\b\i\.\i\.(copy|cut|paste)/g, + replace: "$self.$1" + } + ] + }, + { + find: '"add-to-dictionary"', + predicate: () => settings.store.addBack, + replacement: { + match: /var \i=\i\.text,/, + replace: "return [null,null];$&" + } + } + ], + + async copyImage(url: string) { + // 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; + }, + + async saveImage(url: string) { + const data = await fetchImage(url); + if (!data) return; + + const name = new URL(url).pathname.split("/").pop()!; + const file = new File([data], name, { type: data.type }); + + saveFile(file); + }, + + copy() { + const selection = document.getSelection(); + if (!selection) return; + + Clipboard.copy(selection.toString()); + }, + + cut() { + this.copy(); + MiniDispatcher.dispatch("INSERT_TEXT", { rawText: "" }); + }, + + async paste() { + const text = await navigator.clipboard.readText(); + + const data = new DataTransfer(); + data.setData("text/plain", text); + + document.dispatchEvent( + new ClipboardEvent("paste", { + clipboardData: data + }) + ); + } +}); diff --git a/src/plugins/webKeybinds.vencordDesktop.ts b/src/plugins/webKeybinds.vencordDesktop.ts deleted file mode 100644 index 798713d..0000000 --- a/src/plugins/webKeybinds.vencordDesktop.ts +++ /dev/null @@ -1,79 +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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { findLazy, mapMangledModuleLazy } from "@webpack"; -import { ComponentDispatch, FluxDispatcher, NavigationRouter, SelectedGuildStore, SettingsRouter } from "@webpack/common"; - -const GuildNavBinds = mapMangledModuleLazy("mod+alt+down", { - CtrlTab: m => m.binds?.at(-1) === "ctrl+tab", - CtrlShiftTab: m => m.binds?.at(-1) === "ctrl+shift+tab", -}); - -const DigitBinds = findLazy(m => m.binds?.[0] === "mod+1"); - -export default definePlugin({ - name: "WebKeybinds", - description: "Re-adds keybinds missing in the web version of Discord: ctrl+t, ctrl+shift+t, ctrl+tab, ctrl+shift+tab, ctrl+1-9, ctrl+,", - authors: [Devs.Ven], - enabledByDefault: true, - - onKey(e: KeyboardEvent) { - const hasCtrl = e.ctrlKey || (e.metaKey && navigator.platform.includes("Mac")); - - if (hasCtrl) switch (e.key) { - case "t": - case "T": - e.preventDefault(); - if (e.shiftKey) { - if (SelectedGuildStore.getGuildId()) NavigationRouter.transitionToGuild("@me"); - ComponentDispatch.safeDispatch("TOGGLE_DM_CREATE"); - } else { - FluxDispatcher.dispatch({ - type: "QUICKSWITCHER_SHOW", - query: "", - queryMode: null - }); - } - break; - case ",": - e.preventDefault(); - SettingsRouter.open("My Account"); - break; - case "Tab": - const handler = e.shiftKey ? GuildNavBinds.CtrlShiftTab : GuildNavBinds.CtrlTab; - handler.action(e); - break; - default: - if (e.key >= "1" && e.key <= "9") { - e.preventDefault(); - DigitBinds.action(e, `mod+${e.key}`); - } - break; - } - }, - - start() { - document.addEventListener("keydown", this.onKey); - }, - - stop() { - document.removeEventListener("keydown", this.onKey); - } -}); diff --git a/src/plugins/webKeybinds.vencordDesktop/index.ts b/src/plugins/webKeybinds.vencordDesktop/index.ts new file mode 100644 index 0000000..798713d --- /dev/null +++ b/src/plugins/webKeybinds.vencordDesktop/index.ts @@ -0,0 +1,79 @@ +/* + * 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"; +import { findLazy, mapMangledModuleLazy } from "@webpack"; +import { ComponentDispatch, FluxDispatcher, NavigationRouter, SelectedGuildStore, SettingsRouter } from "@webpack/common"; + +const GuildNavBinds = mapMangledModuleLazy("mod+alt+down", { + CtrlTab: m => m.binds?.at(-1) === "ctrl+tab", + CtrlShiftTab: m => m.binds?.at(-1) === "ctrl+shift+tab", +}); + +const DigitBinds = findLazy(m => m.binds?.[0] === "mod+1"); + +export default definePlugin({ + name: "WebKeybinds", + description: "Re-adds keybinds missing in the web version of Discord: ctrl+t, ctrl+shift+t, ctrl+tab, ctrl+shift+tab, ctrl+1-9, ctrl+,", + authors: [Devs.Ven], + enabledByDefault: true, + + onKey(e: KeyboardEvent) { + const hasCtrl = e.ctrlKey || (e.metaKey && navigator.platform.includes("Mac")); + + if (hasCtrl) switch (e.key) { + case "t": + case "T": + e.preventDefault(); + if (e.shiftKey) { + if (SelectedGuildStore.getGuildId()) NavigationRouter.transitionToGuild("@me"); + ComponentDispatch.safeDispatch("TOGGLE_DM_CREATE"); + } else { + FluxDispatcher.dispatch({ + type: "QUICKSWITCHER_SHOW", + query: "", + queryMode: null + }); + } + break; + case ",": + e.preventDefault(); + SettingsRouter.open("My Account"); + break; + case "Tab": + const handler = e.shiftKey ? GuildNavBinds.CtrlShiftTab : GuildNavBinds.CtrlTab; + handler.action(e); + break; + default: + if (e.key >= "1" && e.key <= "9") { + e.preventDefault(); + DigitBinds.action(e, `mod+${e.key}`); + } + break; + } + }, + + start() { + document.addEventListener("keydown", this.onKey); + }, + + stop() { + document.removeEventListener("keydown", this.onKey); + } +}); diff --git a/src/plugins/whoReacted.tsx b/src/plugins/whoReacted.tsx deleted file mode 100644 index 0bdb5c2..0000000 --- a/src/plugins/whoReacted.tsx +++ /dev/null @@ -1,262 +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 ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { sleep } from "@utils/misc"; -import { Queue } from "@utils/Queue"; -import { LazyComponent, useForceUpdater } from "@utils/react"; -import definePlugin from "@utils/types"; -import { findByCode, findByPropsLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common"; -import { ReactionEmoji, User } from "discord-types/general"; - -const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); -const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); - -const ReactionStore = findByPropsLazy("getReactions"); - -const queue = new Queue(); - -function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) { - const key = emoji.name + (emoji.id ? `:${emoji.id}` : ""); - return RestAPI.get({ - url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`, - query: { - limit: 100, - type - }, - oldFormErrors: true - }) - .then(res => FluxDispatcher.dispatch({ - type: "MESSAGE_REACTION_ADD_USERS", - channelId: msg.channel_id, - messageId: msg.id, - users: res.body, - emoji, - reactionType: type - })) - .catch(console.error) - .finally(() => sleep(250)); -} - -function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) { - const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`; - const cache = ReactionStore.__getLocalVars().reactions[key] ??= { fetched: false, users: {} }; - if (!cache.fetched) { - queue.unshift(() => - fetchReactions(msg, e, type) - ); - cache.fetched = true; - } - - return cache.users; -} - -function makeRenderMoreUsers(users: User[]) { - return function renderMoreUsers(_label: string, _count: number) { - return ( - u.username).join(", ")} > - {({ onMouseEnter, onMouseLeave }) => ( -
- +{users.length - 5} -
- )} -
- ); - }; -} - -function handleClickAvatar(event: React.MouseEvent) { - event.stopPropagation(); -} - -export default definePlugin({ - name: "WhoReacted", - description: "Renders the Avatars of reactors", - authors: [Devs.Ven, Devs.KannaDev], - - patches: [{ - find: ",reactionRef:", - replacement: { - match: /(?<=(\i)=(\i)\.hideCount,)(.+?reactionCount.+?\}\))/, - replace: (_, hideCount, props, rest) => `whoReactedProps=${props},${rest},${hideCount}?null:$self.renderUsers(whoReactedProps)` - } - }], - - renderUsers(props: RootObject) { - return props.message.reactions.length > 10 ? null : ( - - - - ); - }, - - _renderUsers({ message, emoji, type }: RootObject) { - const forceUpdate = useForceUpdater(); - React.useEffect(() => { - const cb = (e: any) => { - if (e.messageId === message.id) - forceUpdate(); - }; - FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb); - - return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb); - }, [message.id]); - - const reactions = getReactionsWithQueue(message, emoji, type); - const users = Object.values(reactions).filter(Boolean) as User[]; - - for (const user of users) { - FluxDispatcher.dispatch({ - type: "USER_UPDATE", - user - }); - } - - return ( -
-
- -
-
- ); - } -}); - - -export interface GuildMemberAvatar { } - -export interface Author { - id: string; - username: string; - discriminator: string; - avatar: string; - avatarDecoration?: any; - email: string; - verified: boolean; - bot: boolean; - system: boolean; - mfaEnabled: boolean; - mobile: boolean; - desktop: boolean; - premiumType: number; - flags: number; - publicFlags: number; - purchasedFlags: number; - premiumUsageFlags: number; - phone: string; - nsfwAllowed: boolean; - guildMemberAvatars: GuildMemberAvatar; -} - -export interface Emoji { - id: string; - name: string; -} - -export interface Reaction { - emoji: Emoji; - count: number; - burst_user_ids: any[]; - burst_count: number; - burst_colors: any[]; - burst_me: boolean; - me: boolean; -} - -export interface Message { - id: string; - type: number; - channel_id: string; - author: Author; - content: string; - deleted: boolean; - editHistory: any[]; - attachments: any[]; - embeds: any[]; - mentions: any[]; - mentionRoles: any[]; - mentionChannels: any[]; - mentioned: boolean; - pinned: boolean; - mentionEveryone: boolean; - tts: boolean; - codedLinks: any[]; - giftCodes: any[]; - timestamp: string; - editedTimestamp?: any; - state: string; - nonce?: any; - blocked: boolean; - call?: any; - bot: boolean; - webhookId?: any; - reactions: Reaction[]; - applicationId?: any; - application?: any; - activity?: any; - messageReference?: any; - flags: number; - isSearchHit: boolean; - stickers: any[]; - stickerItems: any[]; - components: any[]; - loggingName?: any; - interaction?: any; - interactionData?: any; - interactionError?: any; -} - -export interface Emoji { - id: string; - name: string; - animated: boolean; -} - -export interface RootObject { - message: Message; - readOnly: boolean; - isLurking: boolean; - isPendingMember: boolean; - useChatFontScaling: boolean; - emoji: Emoji; - count: number; - burst_user_ids: any[]; - burst_count: number; - burst_colors: any[]; - burst_me: boolean; - me: boolean; - type: number; - hideEmoji: boolean; - remainingBurstCurrency: number; -} diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx new file mode 100644 index 0000000..0bdb5c2 --- /dev/null +++ b/src/plugins/whoReacted/index.tsx @@ -0,0 +1,262 @@ +/* + * 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 ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { sleep } from "@utils/misc"; +import { Queue } from "@utils/Queue"; +import { LazyComponent, useForceUpdater } from "@utils/react"; +import definePlugin from "@utils/types"; +import { findByCode, findByPropsLazy } from "@webpack"; +import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common"; +import { ReactionEmoji, User } from "discord-types/general"; + +const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); +const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); + +const ReactionStore = findByPropsLazy("getReactions"); + +const queue = new Queue(); + +function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) { + const key = emoji.name + (emoji.id ? `:${emoji.id}` : ""); + return RestAPI.get({ + url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`, + query: { + limit: 100, + type + }, + oldFormErrors: true + }) + .then(res => FluxDispatcher.dispatch({ + type: "MESSAGE_REACTION_ADD_USERS", + channelId: msg.channel_id, + messageId: msg.id, + users: res.body, + emoji, + reactionType: type + })) + .catch(console.error) + .finally(() => sleep(250)); +} + +function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) { + const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`; + const cache = ReactionStore.__getLocalVars().reactions[key] ??= { fetched: false, users: {} }; + if (!cache.fetched) { + queue.unshift(() => + fetchReactions(msg, e, type) + ); + cache.fetched = true; + } + + return cache.users; +} + +function makeRenderMoreUsers(users: User[]) { + return function renderMoreUsers(_label: string, _count: number) { + return ( + u.username).join(", ")} > + {({ onMouseEnter, onMouseLeave }) => ( +
+ +{users.length - 5} +
+ )} +
+ ); + }; +} + +function handleClickAvatar(event: React.MouseEvent) { + event.stopPropagation(); +} + +export default definePlugin({ + name: "WhoReacted", + description: "Renders the Avatars of reactors", + authors: [Devs.Ven, Devs.KannaDev], + + patches: [{ + find: ",reactionRef:", + replacement: { + match: /(?<=(\i)=(\i)\.hideCount,)(.+?reactionCount.+?\}\))/, + replace: (_, hideCount, props, rest) => `whoReactedProps=${props},${rest},${hideCount}?null:$self.renderUsers(whoReactedProps)` + } + }], + + renderUsers(props: RootObject) { + return props.message.reactions.length > 10 ? null : ( + + + + ); + }, + + _renderUsers({ message, emoji, type }: RootObject) { + const forceUpdate = useForceUpdater(); + React.useEffect(() => { + const cb = (e: any) => { + if (e.messageId === message.id) + forceUpdate(); + }; + FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb); + + return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb); + }, [message.id]); + + const reactions = getReactionsWithQueue(message, emoji, type); + const users = Object.values(reactions).filter(Boolean) as User[]; + + for (const user of users) { + FluxDispatcher.dispatch({ + type: "USER_UPDATE", + user + }); + } + + return ( +
+
+ +
+
+ ); + } +}); + + +export interface GuildMemberAvatar { } + +export interface Author { + id: string; + username: string; + discriminator: string; + avatar: string; + avatarDecoration?: any; + email: string; + verified: boolean; + bot: boolean; + system: boolean; + mfaEnabled: boolean; + mobile: boolean; + desktop: boolean; + premiumType: number; + flags: number; + publicFlags: number; + purchasedFlags: number; + premiumUsageFlags: number; + phone: string; + nsfwAllowed: boolean; + guildMemberAvatars: GuildMemberAvatar; +} + +export interface Emoji { + id: string; + name: string; +} + +export interface Reaction { + emoji: Emoji; + count: number; + burst_user_ids: any[]; + burst_count: number; + burst_colors: any[]; + burst_me: boolean; + me: boolean; +} + +export interface Message { + id: string; + type: number; + channel_id: string; + author: Author; + content: string; + deleted: boolean; + editHistory: any[]; + attachments: any[]; + embeds: any[]; + mentions: any[]; + mentionRoles: any[]; + mentionChannels: any[]; + mentioned: boolean; + pinned: boolean; + mentionEveryone: boolean; + tts: boolean; + codedLinks: any[]; + giftCodes: any[]; + timestamp: string; + editedTimestamp?: any; + state: string; + nonce?: any; + blocked: boolean; + call?: any; + bot: boolean; + webhookId?: any; + reactions: Reaction[]; + applicationId?: any; + application?: any; + activity?: any; + messageReference?: any; + flags: number; + isSearchHit: boolean; + stickers: any[]; + stickerItems: any[]; + components: any[]; + loggingName?: any; + interaction?: any; + interactionData?: any; + interactionError?: any; +} + +export interface Emoji { + id: string; + name: string; + animated: boolean; +} + +export interface RootObject { + message: Message; + readOnly: boolean; + isLurking: boolean; + isPendingMember: boolean; + useChatFontScaling: boolean; + emoji: Emoji; + count: number; + burst_user_ids: any[]; + burst_count: number; + burst_colors: any[]; + burst_me: boolean; + me: boolean; + type: number; + hideEmoji: boolean; + remainingBurstCurrency: number; +} diff --git a/src/plugins/wikisearch.ts b/src/plugins/wikisearch.ts deleted file mode 100644 index 81dc37a..0000000 --- a/src/plugins/wikisearch.ts +++ /dev/null @@ -1,110 +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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "Wikisearch", - description: "Searches Wikipedia for your requested query. (/wikisearch)", - authors: [Devs.Samu], - dependencies: ["CommandsAPI"], - commands: [ - { - name: "wikisearch", - description: "Searches Wikipedia for your request.", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "search", - description: "Word to search for", - type: ApplicationCommandOptionType.STRING, - required: true - }, - ], - execute: async (_, ctx) => { - const word = findOption(_, "search", ""); - - if (!word) { - return sendBotMessage(ctx.channel.id, { - content: "No word was defined!" - }); - } - - const dataSearchParams = new URLSearchParams({ - action: "query", - format: "json", - list: "search", - formatversion: "2", - origin: "*", - srsearch: word - }); - - const data = await fetch("https://en.wikipedia.org/w/api.php?" + dataSearchParams).then(response => response.json()) - .catch(err => { - console.log(err); - sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" }); - return null; - }); - - if (!data) return; - - if (!data.query?.search?.length) { - console.log(data); - return sendBotMessage(ctx.channel.id, { content: "No results given" }); - } - - const altData = await fetch(`https://en.wikipedia.org/w/api.php?action=query&format=json&prop=info%7Cdescription%7Cimages%7Cimageinfo%7Cpageimages&list=&meta=&indexpageids=1&pageids=${data.query.search[0].pageid}&formatversion=2&origin=*`) - .then(res => res.json()) - .then(data => data.query.pages[0]) - .catch(err => { - console.log(err); - sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" }); - return null; - }); - - if (!altData) return; - - const thumbnailData = altData.thumbnail; - - const thumbnail = thumbnailData && { - url: thumbnailData.source.replace(/(50px-)/ig, "1000px-"), - height: thumbnailData.height * 100, - width: thumbnailData.width * 100 - }; - - sendBotMessage(ctx.channel.id, { - embeds: [ - { - type: "rich", - title: data.query.search[0].title, - url: `https://wikipedia.org/w/index.php?curid=${data.query.search[0].pageid}`, - color: "0x8663BE", - description: data.query.search[0].snippet.replace(/( |<([^>]+)>)/ig, "").replace(/(")/ig, "\"") + "...", - image: thumbnail, - footer: { - text: "Powered by the Wikimedia API", - }, - } - ] as any - }); - } - } - ] -}); diff --git a/src/plugins/wikisearch/index.ts b/src/plugins/wikisearch/index.ts new file mode 100644 index 0000000..81dc37a --- /dev/null +++ b/src/plugins/wikisearch/index.ts @@ -0,0 +1,110 @@ +/* + * 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "Wikisearch", + description: "Searches Wikipedia for your requested query. (/wikisearch)", + authors: [Devs.Samu], + dependencies: ["CommandsAPI"], + commands: [ + { + name: "wikisearch", + description: "Searches Wikipedia for your request.", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [ + { + name: "search", + description: "Word to search for", + type: ApplicationCommandOptionType.STRING, + required: true + }, + ], + execute: async (_, ctx) => { + const word = findOption(_, "search", ""); + + if (!word) { + return sendBotMessage(ctx.channel.id, { + content: "No word was defined!" + }); + } + + const dataSearchParams = new URLSearchParams({ + action: "query", + format: "json", + list: "search", + formatversion: "2", + origin: "*", + srsearch: word + }); + + const data = await fetch("https://en.wikipedia.org/w/api.php?" + dataSearchParams).then(response => response.json()) + .catch(err => { + console.log(err); + sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" }); + return null; + }); + + if (!data) return; + + if (!data.query?.search?.length) { + console.log(data); + return sendBotMessage(ctx.channel.id, { content: "No results given" }); + } + + const altData = await fetch(`https://en.wikipedia.org/w/api.php?action=query&format=json&prop=info%7Cdescription%7Cimages%7Cimageinfo%7Cpageimages&list=&meta=&indexpageids=1&pageids=${data.query.search[0].pageid}&formatversion=2&origin=*`) + .then(res => res.json()) + .then(data => data.query.pages[0]) + .catch(err => { + console.log(err); + sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" }); + return null; + }); + + if (!altData) return; + + const thumbnailData = altData.thumbnail; + + const thumbnail = thumbnailData && { + url: thumbnailData.source.replace(/(50px-)/ig, "1000px-"), + height: thumbnailData.height * 100, + width: thumbnailData.width * 100 + }; + + sendBotMessage(ctx.channel.id, { + embeds: [ + { + type: "rich", + title: data.query.search[0].title, + url: `https://wikipedia.org/w/index.php?curid=${data.query.search[0].pageid}`, + color: "0x8663BE", + description: data.query.search[0].snippet.replace(/( |<([^>]+)>)/ig, "").replace(/(")/ig, "\"") + "...", + image: thumbnail, + footer: { + text: "Powered by the Wikimedia API", + }, + } + ] as any + }); + } + } + ] +}); -- cgit