From 597a74ff6c740e0ff26ca53a56f175b6d06e2e47 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:05:11 -0300 Subject: [PATCH 1/9] ClientTheme: make color picker finder more specific --- src/plugins/clientTheme/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index 7b30863e..d7592996 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -15,7 +15,7 @@ import definePlugin, { OptionType, StartAt } from "@utils/types"; import { findComponentByCodeLazy } from "@webpack"; import { Button, Forms } from "@webpack/common"; -const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR"); +const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const colorPresets = [ "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D", From 9b6308a8355910f5911c1202e6a409636cb9e553 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 Nov 2023 22:12:00 -0300 Subject: [PATCH 2/9] Fix a console shortcut and suppressExperimentalWarnings on more scripts --- package.json | 4 ++-- src/plugins/consoleShortcuts/index.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4d0dd262..d035dcb6 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "doc": "docs" }, "scripts": { - "build": "node scripts/build/build.mjs", + "build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", "generatePluginJson": "tsx scripts/generatePluginList.ts", "inject": "node scripts/runInstaller.mjs", @@ -28,7 +28,7 @@ "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testTsc": "tsc --noEmit", "uninject": "node scripts/runInstaller.mjs", - "watch": "node scripts/build/build.mjs --watch" + "watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch" }, "dependencies": { "@sapphi-red/web-noise-suppressor": "0.3.3", diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index 10853f25..e25e7cb3 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -63,6 +63,7 @@ export default definePlugin({ let fakeRenderWin: WeakRef | undefined; const find = newFindWrapper(f => f); + const findByProps = newFindWrapper(filters.byProps); return { ...Vencord.Webpack.Common, wp: Vencord.Webpack, @@ -73,13 +74,13 @@ export default definePlugin({ wpexs: (code: string) => extract(Webpack.findModuleId(code)!), find, findAll, - findByProps: newFindWrapper(filters.byProps), + findByProps, findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), findByCode: newFindWrapper(filters.byCode), findAllByCode: (code: string) => findAll(filters.byCode(code)), findComponentByCode: newFindWrapper(filters.componentByCode), findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)), - findExportedComponent: (...props: string[]) => find(...props)[props[0]], + findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]], findStore: newFindWrapper(filters.byStoreName), PluginsApi: Vencord.Plugins, plugins: Vencord.Plugins.plugins, From 091d29bf5e89f1a41f5528c520bb6cfac654b3e4 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:14:05 -0300 Subject: [PATCH 3/9] CrashHandler: attempt to prevent more crashes --- src/plugins/crashHandler/index.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts index a1ba01c3..202cac04 100644 --- a/src/plugins/crashHandler/index.ts +++ b/src/plugins/crashHandler/index.ts @@ -24,11 +24,14 @@ 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 { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common"; import type { ReactElement } from "react"; const CrashHandlerLogger = new Logger("CrashHandler"); const ModalStack = findByPropsLazy("pushLazy", "popAll"); +const DraftManager = findByPropsLazy("clearDraft", "saveDraft"); +const { DraftType } = findByPropsLazy("DraftType"); +const { closeExpressionPicker } = findByPropsLazy("closeExpressionPicker", "openExpressionPicker"); const settings = definePluginSettings({ attemptToPreventCrashes: { @@ -115,6 +118,20 @@ export default definePlugin({ } catch { } } + try { + const channelId = SelectedChannelStore.getChannelId(); + + DraftManager.clearDraft(channelId, DraftType.ChannelMessage); + DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage); + } catch (err) { + CrashHandlerLogger.debug("Failed to clear drafts.", err); + } + try { + closeExpressionPicker(); + } + catch (err) { + CrashHandlerLogger.debug("Failed to close expression picker.", err); + } try { FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" }); } catch (err) { From 9945219de70fb70f43577c7df2c231d5b82a23de Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 29 Nov 2023 23:14:52 -0300 Subject: [PATCH 4/9] openInviteModal utility Co-authored-by: AutumnVN --- src/api/Commands/commandHelpers.ts | 4 ++-- src/plugins/greetStickerPicker/index.tsx | 2 +- src/plugins/spotifyShareCommands/index.ts | 4 ++-- src/plugins/voiceMessages/index.tsx | 4 ++-- src/utils/discord.tsx | 17 +++++++++++++++-- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/api/Commands/commandHelpers.ts b/src/api/Commands/commandHelpers.ts index 2fd18903..ebcc4e2f 100644 --- a/src/api/Commands/commandHelpers.ts +++ b/src/api/Commands/commandHelpers.ts @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import { MessageActions } from "@utils/discord"; import { mergeDefaults } from "@utils/misc"; import { findByPropsLazy } from "@webpack"; import { SnowflakeUtils } from "@webpack/common"; @@ -24,7 +25,6 @@ import type { PartialDeep } from "type-fest"; import { Argument } from "./types"; -const MessageCreator = findByPropsLazy("createBotMessage"); const MessageSender = findByPropsLazy("receiveMessage"); export function generateId() { @@ -38,7 +38,7 @@ export function generateId() { * @returns {Message} */ export function sendBotMessage(channelId: string, message: PartialDeep): Message { - const botMessage = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] }); + const botMessage = MessageActions.createBotMessage({ channelId, content: "", embeds: [] }); MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage)); diff --git a/src/plugins/greetStickerPicker/index.tsx b/src/plugins/greetStickerPicker/index.tsx index 9623d422..c2104af4 100644 --- a/src/plugins/greetStickerPicker/index.tsx +++ b/src/plugins/greetStickerPicker/index.tsx @@ -18,6 +18,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; +import { MessageActions } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { ContextMenuApi, FluxDispatcher, Menu } from "@webpack/common"; @@ -49,7 +50,6 @@ const settings = definePluginSettings({ unholyMultiGreetEnabled?: boolean; }>(); -const MessageActions = findByPropsLazy("sendGreetMessage"); const { WELCOME_STICKERS } = findByPropsLazy("WELCOME_STICKERS"); function greet(channel: Channel, message: Message, stickers: string[]) { diff --git a/src/plugins/spotifyShareCommands/index.ts b/src/plugins/spotifyShareCommands/index.ts index 7634e9d5..3569dd28 100644 --- a/src/plugins/spotifyShareCommands/index.ts +++ b/src/plugins/spotifyShareCommands/index.ts @@ -18,6 +18,7 @@ import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands"; import { Devs } from "@utils/constants"; +import { MessageActions } from "@utils/discord"; import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { FluxDispatcher } from "@webpack/common"; @@ -53,7 +54,6 @@ interface Track { } const Spotify = findByPropsLazy("getPlayerState"); -const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage"); const PendingReplyStore = findByPropsLazy("getPendingReply"); function sendMessage(channelId, message) { @@ -65,7 +65,7 @@ function sendMessage(channelId, message) { ...message }; const reply = PendingReplyStore.getPendingReply(channelId); - MessageCreator.sendMessage(channelId, message, void 0, MessageCreator.getSendMessageOptionsForReply(reply)) + MessageActions.sendMessage(channelId, message, void 0, MessageActions.getSendMessageOptionsForReply(reply)) .then(() => { if (reply) { FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId }); diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index 7c8a0694..17e10a4b 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -21,6 +21,7 @@ import "./styles.css"; import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { Microphone } from "@components/Icons"; import { Devs } from "@utils/constants"; +import { MessageActions } from "@utils/discord"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import definePlugin from "@utils/types"; @@ -36,7 +37,6 @@ import { VoicePreview } from "./VoicePreview"; import { VoiceRecorderWeb } from "./WebRecorder"; const CloudUtils = findByPropsLazy("CloudUpload"); -const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage"); const PendingReplyStore = findStoreLazy("PendingReplyStore"); const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel"); @@ -100,7 +100,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) { waveform: meta.waveform, duration_secs: meta.duration, }], - message_reference: reply ? MessageCreator.getSendMessageOptionsForReply(reply)?.messageReference : null, + message_reference: reply ? MessageActions.getSendMessageOptionsForReply(reply)?.messageReference : null, } }); }); diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index 96193f21..41e18f9d 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -23,8 +23,21 @@ import { Guild, Message, User } from "discord-types/general"; import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal"; -const MessageActions = findByPropsLazy("editMessage", "sendMessage"); -const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal"); +export const MessageActions = findByPropsLazy("editMessage", "sendMessage"); +export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal"); +export const InviteActions = findByPropsLazy("resolveInvite"); + +export async function openInviteModal(code: string) { + const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal"); + if (!invite) throw new Error("Invalid invite: " + code); + + FluxDispatcher.dispatch({ + type: "INVITE_MODAL_OPEN", + invite, + code, + context: "APP" + }); +} export function getCurrentChannel() { return ChannelStore.getChannel(SelectedChannelStore.getChannelId()); From 8ef1882d4375c18b485d5afa0432b938e04d27e6 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 30 Nov 2023 00:43:23 -0300 Subject: [PATCH 5/9] use findBulk on CrashHandler and cleaups --- src/plugins/crashHandler/index.ts | 23 ++++++++++++++----- .../components/UserPermissions.tsx | 10 ++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts index 202cac04..9d38b7d1 100644 --- a/src/plugins/crashHandler/index.ts +++ b/src/plugins/crashHandler/index.ts @@ -23,15 +23,26 @@ 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 { filters, findBulk, proxyLazyWebpack } from "@webpack"; import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common"; import type { ReactElement } from "react"; const CrashHandlerLogger = new Logger("CrashHandler"); -const ModalStack = findByPropsLazy("pushLazy", "popAll"); -const DraftManager = findByPropsLazy("clearDraft", "saveDraft"); -const { DraftType } = findByPropsLazy("DraftType"); -const { closeExpressionPicker } = findByPropsLazy("closeExpressionPicker", "openExpressionPicker"); +const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => { + const modules = findBulk( + filters.byProps("pushLazy", "popAll"), + filters.byProps("clearDraft", "saveDraft"), + filters.byProps("DraftType"), + filters.byProps("closeExpressionPicker", "openExpressionPicker"), + ); + + return { + ModalStack: modules[0], + DraftManager: modules[1], + DraftType: modules[2]?.DraftType, + closeExpressionPicker: modules[3]?.closeExpressionPicker, + }; +}); const settings = definePluginSettings({ attemptToPreventCrashes: { @@ -138,7 +149,7 @@ export default definePlugin({ CrashHandlerLogger.debug("Failed to close open context menu.", err); } try { - ModalStack?.popAll(); + ModalStack.popAll(); } catch (err) { CrashHandlerLogger.debug("Failed to close old modals.", err); } diff --git a/src/plugins/permissionsViewer/components/UserPermissions.tsx b/src/plugins/permissionsViewer/components/UserPermissions.tsx index b75bafdc..3c676771 100644 --- a/src/plugins/permissionsViewer/components/UserPermissions.tsx +++ b/src/plugins/permissionsViewer/components/UserPermissions.tsx @@ -35,15 +35,13 @@ interface UserPermission { type UserPermissions = Array; -const Classes = proxyLazyWebpack(() => { - const modules = findBulk( +const Classes = proxyLazyWebpack(() => + Object.assign({}, ...findBulk( filters.byProps("roles", "rolePill", "rolePillBorder"), filters.byProps("roleCircle", "dotBorderBase", "dotBorderColor"), filters.byProps("roleNameOverflow", "root", "roleName", "roleRemoveButton") - ); - - return Object.assign({}, ...modules); -}) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>; + )) +) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>; function UserPermissionsComponent({ guild, guildMember, showBorder }: { guild: Guild; guildMember: GuildMember; showBorder: boolean; }) { const stns = settings.use(["permissionsSortOrder"]); From b47a5f569e3684857048207f0da0d7f6de80c78a Mon Sep 17 00:00:00 2001 From: Jack <30497388+FieryFlames@users.noreply.github.com> Date: Thu, 30 Nov 2023 00:10:50 -0500 Subject: [PATCH 6/9] feat: Add Decor plugin (#910) --- package.json | 3 +- pnpm-lock.yaml | 21 +- src/components/Icons.tsx | 35 +++ src/plugins/decor/README.md | 17 ++ src/plugins/decor/index.tsx | 168 +++++++++++ src/plugins/decor/lib/api.ts | 83 ++++++ src/plugins/decor/lib/constants.ts | 16 ++ .../decor/lib/stores/AuthorizationStore.tsx | 102 +++++++ .../lib/stores/CurrentUserDecorationsStore.ts | 56 ++++ .../decor/lib/stores/UsersDecorationsStore.ts | 118 ++++++++ src/plugins/decor/lib/utils/decoration.ts | 17 ++ .../DecorDecorationGridDecoration.tsx | 35 +++ .../decor/ui/components/DecorSection.tsx | 59 ++++ .../ui/components/DecorationContextMenu.tsx | 47 +++ .../ui/components/DecorationGridCreate.tsx | 30 ++ .../ui/components/DecorationGridNone.tsx | 30 ++ src/plugins/decor/ui/components/Grid.tsx | 28 ++ .../decor/ui/components/SectionedGridList.tsx | 38 +++ src/plugins/decor/ui/components/index.ts | 33 +++ src/plugins/decor/ui/index.ts | 13 + .../decor/ui/modals/ChangeDecorationModal.tsx | 270 ++++++++++++++++++ .../decor/ui/modals/CreateDecorationModal.tsx | 163 +++++++++++ src/plugins/decor/ui/styles.css | 80 ++++++ src/utils/cloud.tsx | 5 +- src/utils/discord.tsx | 24 +- src/webpack/common/components.ts | 4 +- src/webpack/common/types/components.d.ts | 1 + src/webpack/common/utils.ts | 10 +- tsconfig.json | 1 + 29 files changed, 1493 insertions(+), 14 deletions(-) create mode 100644 src/plugins/decor/README.md create mode 100644 src/plugins/decor/index.tsx create mode 100644 src/plugins/decor/lib/api.ts create mode 100644 src/plugins/decor/lib/constants.ts create mode 100644 src/plugins/decor/lib/stores/AuthorizationStore.tsx create mode 100644 src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts create mode 100644 src/plugins/decor/lib/stores/UsersDecorationsStore.ts create mode 100644 src/plugins/decor/lib/utils/decoration.ts create mode 100644 src/plugins/decor/ui/components/DecorDecorationGridDecoration.tsx create mode 100644 src/plugins/decor/ui/components/DecorSection.tsx create mode 100644 src/plugins/decor/ui/components/DecorationContextMenu.tsx create mode 100644 src/plugins/decor/ui/components/DecorationGridCreate.tsx create mode 100644 src/plugins/decor/ui/components/DecorationGridNone.tsx create mode 100644 src/plugins/decor/ui/components/Grid.tsx create mode 100644 src/plugins/decor/ui/components/SectionedGridList.tsx create mode 100644 src/plugins/decor/ui/components/index.ts create mode 100644 src/plugins/decor/ui/index.ts create mode 100644 src/plugins/decor/ui/modals/ChangeDecorationModal.tsx create mode 100644 src/plugins/decor/ui/modals/CreateDecorationModal.tsx create mode 100644 src/plugins/decor/ui/styles.css diff --git a/package.json b/package.json index d035dcb6..af472093 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "tsx": "^3.12.7", "type-fest": "^3.9.0", "typescript": "^5.0.4", - "zip-local": "^0.3.5" + "zip-local": "^0.3.5", + "zustand": "^3.7.2" }, "packageManager": "pnpm@8.10.2", "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be7befab..43866f50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - patchedDependencies: eslint-plugin-path-alias@1.0.0: hash: m6sma4g6bh67km3q6igf6uxaja @@ -123,6 +119,9 @@ devDependencies: zip-local: specifier: ^0.3.5 version: 0.3.5 + zustand: + specifier: ^3.7.2 + version: 3.7.2 packages: @@ -3450,8 +3449,22 @@ packages: q: 1.5.1 dev: true + /zustand@3.7.2: + resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + dev: true + github.com/mattdesl/gifenc/64842fca317b112a8590f8fef2bf3825da8f6fe3: resolution: {tarball: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3} name: gifenc version: 1.0.3 dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index 93b1323e..2eb83d4e 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -255,3 +255,38 @@ export function DeleteIcon(props: IconProps) { ); } + +export function PlusIcon(props: IconProps) { + return ( + + + + ); +} + +export function NoEntrySignIcon(props: IconProps) { + return ( + + + + + ); +} diff --git a/src/plugins/decor/README.md b/src/plugins/decor/README.md new file mode 100644 index 00000000..467a6145 --- /dev/null +++ b/src/plugins/decor/README.md @@ -0,0 +1,17 @@ +# Decor + +Custom avatar decorations! + +![Custom decorations in chat](https://github.com/Vendicated/Vencord/assets/30497388/b0c4c4c8-8723-42a8-b50f-195ad4e26136) + +Create and use your own custom avatar decorations, or pick your favorite from the presets. + +You'll be able to see the custom avatar decorations of other users of this plugin, and they'll be able to see your custom avatar decoration. + +You can select and manage your custom avatar decorations under the "Profiles" page in settings, or in the plugin settings. + +![Custom decorations management](https://github.com/Vendicated/Vencord/assets/30497388/74fe8a9e-a2a2-4b29-bc10-9eaa58208ad4) + +Review the [guidelines](https://github.com/decor-discord/.github/blob/main/GUIDELINES.md) before creating your own custom avatar decoration. + +Join the [Discord server](https://discord.gg/dXp2SdxDcP) for support and notifications on your decoration's review. diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx new file mode 100644 index 00000000..4dd7aa0c --- /dev/null +++ b/src/plugins/decor/index.tsx @@ -0,0 +1,168 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated, FieryFlames and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./ui/styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Link } from "@components/Link"; +import { Devs } from "@utils/constants"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { closeAllModals } from "@utils/modal"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { FluxDispatcher, Forms, UserStore } from "@webpack/common"; + +import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants"; +import { useAuthorizationStore } from "./lib/stores/AuthorizationStore"; +import { useCurrentUserDecorationsStore } from "./lib/stores/CurrentUserDecorationsStore"; +import { useUserDecorAvatarDecoration, useUsersDecorationsStore } from "./lib/stores/UsersDecorationsStore"; +import { setDecorationGridDecoration, setDecorationGridItem } from "./ui/components"; +import DecorSection from "./ui/components/DecorSection"; + +const { isAnimatedAvatarDecoration } = findByPropsLazy("isAnimatedAvatarDecoration"); +export interface AvatarDecoration { + asset: string; + skuId: string; +} + +const settings = definePluginSettings({ + changeDecoration: { + type: OptionType.COMPONENT, + description: "Change your avatar decoration", + component() { + return
+ + + You can also access Decor decorations from the { + e.preventDefault(); + closeAllModals(); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" }); + }} + >Profiles page. + +
; + } + } +}); +export default definePlugin({ + name: "Decor", + description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.", + authors: [Devs.FieryFlames], + patches: [ + // Patch MediaResolver to return correct URL for Decor avatar decorations + { + find: "getAvatarDecorationURL:", + replacement: { + match: /(?<=function \i\(\i\){)(?=let{avatarDecoration)/, + replace: "const vcDecorDecoration=$self.getDecorAvatarDecorationURL(arguments[0]);if(vcDecorDecoration)return vcDecorDecoration;" + } + }, + // Patch profile customization settings to include Decor section + { + find: "DefaultCustomizationSections", + replacement: { + match: /(?<={user:\i},"decoration"\),)/, + replace: "$self.DecorSection()," + } + }, + // Decoration modal module + { + find: ".decorationGridItem", + replacement: [ + { + match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/, + replace: "$self.DecorationGridItem=$&" + }, + { + match: /(?<==)\i=>{let{user:\i,avatarDecoration.{300,600}decorationGridItemChurned/, + replace: "$self.DecorationGridDecoration=$&" + }, + // Remove NEW label from decor avatar decorations + { + match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i;if\()(?<=avatarDecoration:(\i).+?)/, + replace: "$1.skuId===$self.SKU_ID||" + } + ] + }, + { + find: "isAvatarDecorationAnimating:", + group: true, + replacement: [ + // Add Decor avatar decoration hook to avatar decoration hook + { + match: /(?<=TryItOut:\i}\),)(?<=user:(\i).+?)/, + replace: "vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1)," + }, + // Use added hook + { + match: /(?<={avatarDecoration:).{1,20}?(?=,)(?<=avatarDecorationOverride:(\i).+?)/, + replace: "$1??vcDecorAvatarDecoration??($&)" + }, + // Make memo depend on added hook + { + match: /(?<=size:\i}\),\[)/, + replace: "vcDecorAvatarDecoration," + } + ] + }, + // Current user area, at bottom of channels/dm list + { + find: "renderAvatarWithPopout(){", + replacement: [ + // Use Decor avatar decoration hook + { + match: /(?<=getAvatarDecorationURL\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/, + replace: "$self.useUserDecorAvatarDecoration($1)??$&" + } + ] + } + ], + settings, + + flux: { + CONNECTION_OPEN: () => { + useAuthorizationStore.getState().init(); + useCurrentUserDecorationsStore.getState().clear(); + useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true); + }, + USER_PROFILE_MODAL_OPEN: data => { + useUsersDecorationsStore.getState().fetch(data.userId, true); + }, + }, + + set DecorationGridItem(e: any) { + setDecorationGridItem(e); + }, + + set DecorationGridDecoration(e: any) { + setDecorationGridDecoration(e); + }, + + SKU_ID, + + useUserDecorAvatarDecoration, + + async start() { + useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true); + }, + + getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) { + // Only Decor avatar decorations have this SKU ID + if (avatarDecoration?.skuId === SKU_ID) { + const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`); + url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString()); + return url.toString(); + } else if (avatarDecoration?.skuId === RAW_SKU_ID) { + return avatarDecoration.asset; + } + }, + + DecorSection: ErrorBoundary.wrap(DecorSection) +}); diff --git a/src/plugins/decor/lib/api.ts b/src/plugins/decor/lib/api.ts new file mode 100644 index 00000000..3719cf24 --- /dev/null +++ b/src/plugins/decor/lib/api.ts @@ -0,0 +1,83 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { API_URL } from "./constants"; +import { useAuthorizationStore } from "./stores/AuthorizationStore"; + +export interface Preset { + id: string; + name: string; + description: string | null; + decorations: Decoration[]; + authorIds: string[]; +} + +export interface Decoration { + hash: string; + animated: boolean; + alt: string | null; + authorId: string | null; + reviewed: boolean | null; + presetId: string | null; +} + +export interface NewDecoration { + file: File; + alt: string | null; +} + +export async function fetchApi(url: RequestInfo, options?: RequestInit) { + const res = await fetch(url, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${useAuthorizationStore.getState().token}` + } + }); + + if (res.ok) return res; + else throw new Error(await res.text()); +} + +export const getUsersDecorations = async (ids?: string[]): Promise> => { + if (ids?.length === 0) return {}; + + const url = new URL(API_URL + "/users"); + if (ids && ids.length !== 0) url.searchParams.set("ids", JSON.stringify(ids)); + + return await fetch(url).then(c => c.json()); +}; + +export const getUserDecorations = async (id: string = "@me"): Promise => + fetchApi(API_URL + `/users/${id}/decorations`).then(c => c.json()); + +export const getUserDecoration = async (id: string = "@me"): Promise => + fetchApi(API_URL + `/users/${id}/decoration`).then(c => c.json()); + +export const setUserDecoration = async (decoration: Decoration | NewDecoration | null, id: string = "@me"): Promise => { + const formData = new FormData(); + + if (!decoration) { + formData.append("hash", "null"); + } else if ("hash" in decoration) { + formData.append("hash", decoration.hash); + } else if ("file" in decoration) { + formData.append("image", decoration.file); + formData.append("alt", decoration.alt ?? "null"); + } + + return fetchApi(API_URL + `/users/${id}/decoration`, { method: "PUT", body: formData }).then(c => + decoration && "file" in decoration ? c.json() : c.text() + ); +}; + +export const getDecoration = async (hash: string): Promise => fetch(API_URL + `/decorations/${hash}`).then(c => c.json()); + +export const deleteDecoration = async (hash: string): Promise => { + await fetchApi(API_URL + `/decorations/${hash}`, { method: "DELETE" }); +}; + +export const getPresets = async (): Promise => fetch(API_URL + "/decorations/presets").then(c => c.json()); diff --git a/src/plugins/decor/lib/constants.ts b/src/plugins/decor/lib/constants.ts new file mode 100644 index 00000000..ce0b5979 --- /dev/null +++ b/src/plugins/decor/lib/constants.ts @@ -0,0 +1,16 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export const BASE_URL = "https://decor.fieryflames.dev"; +export const API_URL = BASE_URL + "/api"; +export const AUTHORIZE_URL = API_URL + "/authorize"; +export const CDN_URL = "https://ugc.decor.fieryflames.dev"; +export const CLIENT_ID = "1096966363416899624"; +export const SKU_ID = "100101099111114"; // decor in ascii numbers +export const RAW_SKU_ID = "11497119"; // raw in ascii numbers +export const GUILD_ID = "1096357702931841148"; +export const INVITE_KEY = "dXp2SdxDcP"; +export const DECORATION_FETCH_COOLDOWN = 1000 * 60 * 60 * 4; // 4 hours diff --git a/src/plugins/decor/lib/stores/AuthorizationStore.tsx b/src/plugins/decor/lib/stores/AuthorizationStore.tsx new file mode 100644 index 00000000..e31b1f43 --- /dev/null +++ b/src/plugins/decor/lib/stores/AuthorizationStore.tsx @@ -0,0 +1,102 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { DataStore } from "@api/index"; +import { proxyLazy } from "@utils/lazy"; +import { Logger } from "@utils/Logger"; +import { openModal } from "@utils/modal"; +import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common"; +import type { StateStorage } from "zustand/middleware"; + +import { AUTHORIZE_URL, CLIENT_ID } from "../constants"; + +interface AuthorizationState { + token: string | null; + tokens: Record; + init: () => void; + authorize: () => Promise; + setToken: (token: string) => void; + remove: (id: string) => void; + isAuthorized: () => boolean; +} + +const indexedDBStorage: StateStorage = { + async getItem(name: string): Promise { + return DataStore.get(name).then(v => v ?? null); + }, + async setItem(name: string, value: string): Promise { + await DataStore.set(name, value); + }, + async removeItem(name: string): Promise { + await DataStore.del(name); + }, +}; + +// TODO: Move switching accounts subscription inside the store? +export const useAuthorizationStore = proxyLazy(() => zustandCreate( + zustandPersist( + (set, get) => ({ + token: null, + tokens: {}, + init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); }, + setToken: (token: string) => set({ token, tokens: { ...get().tokens, [UserStore.getCurrentUser().id]: token } }), + remove: (id: string) => { + const { tokens, init } = get(); + const newTokens = { ...tokens }; + delete newTokens[id]; + set({ tokens: newTokens }); + + init(); + }, + async authorize() { + return new Promise((resolve, reject) => openModal(props => + { + try { + const url = new URL(response.location); + url.searchParams.append("client", "vencord"); + + const req = await fetch(url); + + if (req?.ok) { + const token = await req.text(); + get().setToken(token); + } else { + throw new Error("Request not OK"); + } + resolve(void 0); + } catch (e) { + if (e instanceof Error) { + showToast(`Failed to authorize: ${e.message}`, Toasts.Type.FAILURE); + new Logger("Decor").error("Failed to authorize", e); + reject(e); + } + } + }} + />, { + onCloseCallback() { + reject(new Error("Authorization cancelled")); + }, + } + )); + }, + isAuthorized: () => !!get().token, + }), + { + name: "decor-auth", + getStorage: () => indexedDBStorage, + partialize: state => ({ tokens: state.tokens }), + onRehydrateStorage: () => state => state?.init() + } + ) +)); diff --git a/src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts b/src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts new file mode 100644 index 00000000..1485a743 --- /dev/null +++ b/src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts @@ -0,0 +1,56 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { proxyLazy } from "@utils/lazy"; +import { UserStore, zustandCreate } from "@webpack/common"; + +import { Decoration, deleteDecoration, getUserDecoration, getUserDecorations, NewDecoration, setUserDecoration } from "../api"; +import { decorationToAsset } from "../utils/decoration"; +import { useUsersDecorationsStore } from "./UsersDecorationsStore"; + +interface UserDecorationsState { + decorations: Decoration[]; + selectedDecoration: Decoration | null; + fetch: () => Promise; + delete: (decoration: Decoration | string) => Promise; + create: (decoration: NewDecoration) => Promise; + select: (decoration: Decoration | null) => Promise; + clear: () => void; +} + +export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate((set, get) => ({ + decorations: [], + selectedDecoration: null, + async fetch() { + const decorations = await getUserDecorations(); + const selectedDecoration = await getUserDecoration(); + + set({ decorations, selectedDecoration }); + }, + async create(newDecoration: NewDecoration) { + const decoration = (await setUserDecoration(newDecoration)) as Decoration; + set({ decorations: [...get().decorations, decoration] }); + }, + async delete(decoration: Decoration | string) { + const hash = typeof decoration === "object" ? decoration.hash : decoration; + await deleteDecoration(hash); + + const { selectedDecoration, decorations } = get(); + const newState = { + decorations: decorations.filter(d => d.hash !== hash), + selectedDecoration: selectedDecoration?.hash === hash ? null : selectedDecoration + }; + + set(newState); + }, + async select(decoration: Decoration | null) { + if (get().selectedDecoration === decoration) return; + set({ selectedDecoration: decoration }); + setUserDecoration(decoration); + useUsersDecorationsStore.getState().set(UserStore.getCurrentUser().id, decoration ? decorationToAsset(decoration) : null); + }, + clear: () => set({ decorations: [], selectedDecoration: null }) +}))); diff --git a/src/plugins/decor/lib/stores/UsersDecorationsStore.ts b/src/plugins/decor/lib/stores/UsersDecorationsStore.ts new file mode 100644 index 00000000..7295a3b1 --- /dev/null +++ b/src/plugins/decor/lib/stores/UsersDecorationsStore.ts @@ -0,0 +1,118 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { debounce } from "@utils/debounce"; +import { proxyLazy } from "@utils/lazy"; +import { useEffect, useState, zustandCreate } from "@webpack/common"; +import { User } from "discord-types/general"; + +import { AvatarDecoration } from "../../"; +import { getUsersDecorations } from "../api"; +import { DECORATION_FETCH_COOLDOWN, SKU_ID } from "../constants"; + +interface UserDecorationData { + asset: string | null; + fetchedAt: Date; +} + +interface UsersDecorationsState { + usersDecorations: Map; + fetchQueue: Set; + bulkFetch: () => Promise; + fetch: (userId: string, force?: boolean) => Promise; + fetchMany: (userIds: string[]) => Promise; + get: (userId: string) => UserDecorationData | undefined; + getAsset: (userId: string) => string | null | undefined; + has: (userId: string) => boolean; + set: (userId: string, decoration: string | null) => void; +} + +export const useUsersDecorationsStore = proxyLazy(() => zustandCreate((set, get) => ({ + usersDecorations: new Map(), + fetchQueue: new Set(), + bulkFetch: debounce(async () => { + const { fetchQueue, usersDecorations } = get(); + + if (fetchQueue.size === 0) return; + + set({ fetchQueue: new Set() }); + + const fetchIds = Array.from(fetchQueue); + const fetchedUsersDecorations = await getUsersDecorations(fetchIds); + + const newUsersDecorations = new Map(usersDecorations); + + const now = new Date(); + for (const fetchId of fetchIds) { + const newDecoration = fetchedUsersDecorations[fetchId] ?? null; + newUsersDecorations.set(fetchId, { asset: newDecoration, fetchedAt: now }); + } + + set({ usersDecorations: newUsersDecorations }); + }), + async fetch(userId: string, force: boolean = false) { + const { usersDecorations, fetchQueue, bulkFetch } = get(); + + const { fetchedAt } = usersDecorations.get(userId) ?? {}; + if (fetchedAt) { + if (!force && Date.now() - fetchedAt.getTime() < DECORATION_FETCH_COOLDOWN) return; + } + + set({ fetchQueue: new Set(fetchQueue).add(userId) }); + bulkFetch(); + }, + async fetchMany(userIds) { + if (!userIds.length) return; + const { usersDecorations, fetchQueue, bulkFetch } = get(); + + const newFetchQueue = new Set(fetchQueue); + + const now = Date.now(); + for (const userId of userIds) { + const { fetchedAt } = usersDecorations.get(userId) ?? {}; + if (fetchedAt) { + if (now - fetchedAt.getTime() < DECORATION_FETCH_COOLDOWN) continue; + } + newFetchQueue.add(userId); + } + + set({ fetchQueue: newFetchQueue }); + bulkFetch(); + }, + get(userId: string) { return get().usersDecorations.get(userId); }, + getAsset(userId: string) { return get().usersDecorations.get(userId)?.asset; }, + has(userId: string) { return get().usersDecorations.has(userId); }, + set(userId: string, decoration: string | null) { + const { usersDecorations } = get(); + const newUsersDecorations = new Map(usersDecorations); + + newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() }); + set({ usersDecorations: newUsersDecorations }); + } +}))); + +export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | null | undefined { + const [decorAvatarDecoration, setDecorAvatarDecoration] = useState(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null); + + useEffect(() => { + const destructor = useUsersDecorationsStore.subscribe( + state => { + if (!user) return; + const newDecorAvatarDecoration = state.getAsset(user.id); + if (!newDecorAvatarDecoration) return; + if (decorAvatarDecoration !== newDecorAvatarDecoration) setDecorAvatarDecoration(newDecorAvatarDecoration); + } + ); + + if (user) { + const { fetch: fetchUserDecorAvatarDecoration } = useUsersDecorationsStore.getState(); + fetchUserDecorAvatarDecoration(user.id); + } + return destructor; + }, []); + + return decorAvatarDecoration ? { asset: decorAvatarDecoration, skuId: SKU_ID } : null; +} diff --git a/src/plugins/decor/lib/utils/decoration.ts b/src/plugins/decor/lib/utils/decoration.ts new file mode 100644 index 00000000..176507ef --- /dev/null +++ b/src/plugins/decor/lib/utils/decoration.ts @@ -0,0 +1,17 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { AvatarDecoration } from "../../"; +import { Decoration } from "../api"; +import { SKU_ID } from "../constants"; + +export function decorationToAsset(decoration: Decoration) { + return `${decoration.animated ? "a_" : ""}${decoration.hash}`; +} + +export function decorationToAvatarDecoration(decoration: Decoration): AvatarDecoration { + return { asset: decorationToAsset(decoration), skuId: SKU_ID }; +} diff --git a/src/plugins/decor/ui/components/DecorDecorationGridDecoration.tsx b/src/plugins/decor/ui/components/DecorDecorationGridDecoration.tsx new file mode 100644 index 00000000..deaeef63 --- /dev/null +++ b/src/plugins/decor/ui/components/DecorDecorationGridDecoration.tsx @@ -0,0 +1,35 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ContextMenuApi } from "@webpack/common"; +import type { HTMLProps } from "react"; + +import { Decoration } from "../../lib/api"; +import { decorationToAvatarDecoration } from "../../lib/utils/decoration"; +import { DecorationGridDecoration } from "."; +import DecorationContextMenu from "./DecorationContextMenu"; + +interface DecorDecorationGridDecorationProps extends HTMLProps { + decoration: Decoration; + isSelected: boolean; + onSelect: () => void; +} + +export default function DecorDecorationGridDecoration(props: DecorDecorationGridDecorationProps) { + const { decoration } = props; + + return { + ContextMenuApi.openContextMenu(e, () => ( + + )); + }} + avatarDecoration={decorationToAvatarDecoration(decoration)} + />; +} diff --git a/src/plugins/decor/ui/components/DecorSection.tsx b/src/plugins/decor/ui/components/DecorSection.tsx new file mode 100644 index 00000000..f11a87a5 --- /dev/null +++ b/src/plugins/decor/ui/components/DecorSection.tsx @@ -0,0 +1,59 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Flex } from "@components/Flex"; +import { findByCodeLazy } from "@webpack"; +import { Button, useEffect } from "@webpack/common"; + +import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore"; +import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; +import { cl } from "../"; +import { openChangeDecorationModal } from "../modals/ChangeDecorationModal"; + +const CustomizationSection = findByCodeLazy(".customizationSectionBackground"); + +interface DecorSectionProps { + hideTitle?: boolean; + hideDivider?: boolean; + noMargin?: boolean; +} + +export default function DecorSection({ hideTitle = false, hideDivider = false, noMargin = false }: DecorSectionProps) { + const authorization = useAuthorizationStore(); + const { selectedDecoration, select: selectDecoration, fetch: fetchDecorations } = useCurrentUserDecorationsStore(); + + useEffect(() => { + if (authorization.isAuthorized()) fetchDecorations(); + }, [authorization.token]); + + return + + + {selectedDecoration && authorization.isAuthorized() && } + + ; +} diff --git a/src/plugins/decor/ui/components/DecorationContextMenu.tsx b/src/plugins/decor/ui/components/DecorationContextMenu.tsx new file mode 100644 index 00000000..7451bb22 --- /dev/null +++ b/src/plugins/decor/ui/components/DecorationContextMenu.tsx @@ -0,0 +1,47 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { CopyIcon, DeleteIcon } from "@components/Icons"; +import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "webpack/common"; + +import { Decoration } from "../../lib/api"; +import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; +import { cl } from "../"; + +export default function DecorationContextMenu({ decoration }: { decoration: Decoration; }) { + const { delete: deleteDecoration } = useCurrentUserDecorationsStore(); + + return + Clipboard.copy(decoration.hash)} + /> + {decoration.authorId === UserStore.getCurrentUser().id && + Alerts.show({ + title: "Delete Decoration", + body: `Are you sure you want to delete ${decoration.alt}?`, + confirmText: "Delete", + confirmColor: cl("danger-btn"), + cancelText: "Cancel", + onConfirm() { + deleteDecoration(decoration); + } + })} + /> + } + ; +} diff --git a/src/plugins/decor/ui/components/DecorationGridCreate.tsx b/src/plugins/decor/ui/components/DecorationGridCreate.tsx new file mode 100644 index 00000000..7699b23d --- /dev/null +++ b/src/plugins/decor/ui/components/DecorationGridCreate.tsx @@ -0,0 +1,30 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { PlusIcon } from "@components/Icons"; +import { i18n, Text } from "@webpack/common"; +import { HTMLProps } from "react"; + +import { DecorationGridItem } from "."; + +type DecorationGridCreateProps = HTMLProps & { + onSelect: () => void; +}; + +export default function DecorationGridCreate(props: DecorationGridCreateProps) { + return + + + {i18n.Messages.CREATE} + + ; +} diff --git a/src/plugins/decor/ui/components/DecorationGridNone.tsx b/src/plugins/decor/ui/components/DecorationGridNone.tsx new file mode 100644 index 00000000..b6114c67 --- /dev/null +++ b/src/plugins/decor/ui/components/DecorationGridNone.tsx @@ -0,0 +1,30 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { NoEntrySignIcon } from "@components/Icons"; +import { i18n, Text } from "@webpack/common"; +import { HTMLProps } from "react"; + +import { DecorationGridItem } from "."; + +type DecorationGridNoneProps = HTMLProps & { + isSelected: boolean; + onSelect: () => void; +}; + +export default function DecorationGridNone(props: DecorationGridNoneProps) { + return + + + {i18n.Messages.NONE} + + ; +} diff --git a/src/plugins/decor/ui/components/Grid.tsx b/src/plugins/decor/ui/components/Grid.tsx new file mode 100644 index 00000000..40180248 --- /dev/null +++ b/src/plugins/decor/ui/components/Grid.tsx @@ -0,0 +1,28 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { React } from "@webpack/common"; + +import { cl } from "../"; + +export interface GridProps { + renderItem: (item: ItemT) => JSX.Element; + getItemKey: (item: ItemT) => string; + itemKeyPrefix?: string; + items: Array; +} + +export default function Grid({ renderItem, getItemKey, itemKeyPrefix: ikp, items }: GridProps) { + return
+ {items.map(item => + + {renderItem(item)} + + )} +
; +} diff --git a/src/plugins/decor/ui/components/SectionedGridList.tsx b/src/plugins/decor/ui/components/SectionedGridList.tsx new file mode 100644 index 00000000..9a6ec1b8 --- /dev/null +++ b/src/plugins/decor/ui/components/SectionedGridList.tsx @@ -0,0 +1,38 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classes } from "@utils/misc"; +import { findByPropsLazy } from "@webpack"; +import { React } from "@webpack/common"; + +import { cl } from "../"; +import Grid, { GridProps } from "./Grid"; + +const ScrollerClasses = findByPropsLazy("managedReactiveScroller"); + +type Section = SectionT & { + items: Array; +}; + +interface SectionedGridListProps> extends Omit, "items"> { + renderSectionHeader: (section: SectionU) => JSX.Element; + getSectionKey: (section: SectionU) => string; + sections: SectionU[]; +} + +export default function SectionedGridList(props: SectionedGridListProps) { + return
+ {props.sections.map(section =>
+ {props.renderSectionHeader(section)} + +
)} +
; +} diff --git a/src/plugins/decor/ui/components/index.ts b/src/plugins/decor/ui/components/index.ts new file mode 100644 index 00000000..8f39a10e --- /dev/null +++ b/src/plugins/decor/ui/components/index.ts @@ -0,0 +1,33 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findComponentByCode, LazyComponentWebpack } from "@webpack"; +import { React } from "@webpack/common"; +import type { ComponentType, HTMLProps, PropsWithChildren } from "react"; + +import { AvatarDecoration } from "../.."; + +type DecorationGridItemComponent = ComponentType> & { + onSelect: () => void, + isSelected: boolean, +}>; + +export let DecorationGridItem: DecorationGridItemComponent; +export const setDecorationGridItem = v => DecorationGridItem = v; + +export const AvatarDecorationModalPreview = LazyComponentWebpack(() => { + const component = findComponentByCode("AvatarDecorationModalPreview"); + return React.memo(component); +}); + +type DecorationGridDecorationComponent = React.ComponentType & { + avatarDecoration: AvatarDecoration; + onSelect: () => void, + isSelected: boolean, +}>; + +export let DecorationGridDecoration: DecorationGridDecorationComponent; +export const setDecorationGridDecoration = v => DecorationGridDecoration = v; diff --git a/src/plugins/decor/ui/index.ts b/src/plugins/decor/ui/index.ts new file mode 100644 index 00000000..52b169d7 --- /dev/null +++ b/src/plugins/decor/ui/index.ts @@ -0,0 +1,13 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { extractAndLoadChunksLazy } from "@webpack"; + +export const cl = classNameFactory("vc-decor-"); + +export const requireAvatarDecorationModal = extractAndLoadChunksLazy(["openAvatarDecorationModal:"]); +export const requireCreateStickerModal = extractAndLoadChunksLazy(["stickerInspected]:"]); diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx new file mode 100644 index 00000000..bed00717 --- /dev/null +++ b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx @@ -0,0 +1,270 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Flex } from "@components/Flex"; +import { openInviteModal } from "@utils/discord"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common"; +import { User } from "discord-types/general"; + +import { Decoration, getPresets, Preset } from "../../lib/api"; +import { GUILD_ID, INVITE_KEY } from "../../lib/constants"; +import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore"; +import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; +import { decorationToAvatarDecoration } from "../../lib/utils/decoration"; +import { cl, requireAvatarDecorationModal } from "../"; +import { AvatarDecorationModalPreview } from "../components"; +import DecorationGridCreate from "../components/DecorationGridCreate"; +import DecorationGridNone from "../components/DecorationGridNone"; +import DecorDecorationGridDecoration from "../components/DecorDecorationGridDecoration"; +import SectionedGridList from "../components/SectionedGridList"; +import { openCreateDecorationModal } from "./CreateDecorationModal"; + +const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); +const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); + +function usePresets() { + const [presets, setPresets] = useState([]); + useEffect(() => { getPresets().then(setPresets); }, []); + return presets; +} + +interface Section { + title: string; + subtitle?: string; + sectionKey: string; + items: ("none" | "create" | Decoration)[]; + authorIds?: string[]; +} + +function SectionHeader({ section }: { section: Section; }) { + const hasSubtitle = typeof section.subtitle !== "undefined"; + const hasAuthorIds = typeof section.authorIds !== "undefined"; + + const [authors, setAuthors] = useState([]); + + useEffect(() => { + (async () => { + if (!section.authorIds) return; + + for (const authorId of section.authorIds) { + const author = UserStore.getUser(authorId) ?? await UserUtils.getUser(authorId); + setAuthors(authors => [...authors, author]); + } + })(); + }, [section.authorIds]); + + return
+ + {section.title} + {hasAuthorIds && + } + + {hasSubtitle && + + {section.subtitle} + + } +
; +} + +export default function ChangeDecorationModal(props: any) { + // undefined = not trying, null = none, Decoration = selected + const [tryingDecoration, setTryingDecoration] = useState(undefined); + const isTryingDecoration = typeof tryingDecoration !== "undefined"; + + const avatarDecorationOverride = tryingDecoration != null ? decorationToAvatarDecoration(tryingDecoration) : tryingDecoration; + + const { + decorations, + selectedDecoration, + fetch: fetchUserDecorations, + select: selectDecoration + } = useCurrentUserDecorationsStore(); + + useEffect(() => { + fetchUserDecorations(); + }, []); + + const activeSelectedDecoration = isTryingDecoration ? tryingDecoration : selectedDecoration; + const activeDecorationHasAuthor = typeof activeSelectedDecoration?.authorId !== "undefined"; + const hasDecorationPendingReview = decorations.some(d => d.reviewed === false); + + const presets = usePresets(); + const presetDecorations = presets.flatMap(preset => preset.decorations); + + const activeDecorationPreset = presets.find(preset => preset.id === activeSelectedDecoration?.presetId); + const isActiveDecorationPreset = typeof activeDecorationPreset !== "undefined"; + + const ownDecorations = decorations.filter(d => !presetDecorations.some(p => p.hash === d.hash)); + + const data = [ + { + title: "Your Decorations", + sectionKey: "ownDecorations", + items: ["none", ...ownDecorations, "create"] + }, + ...presets.map(preset => ({ + title: preset.name, + subtitle: preset.description || undefined, + sectionKey: `preset-${preset.id}`, + items: preset.decorations, + authorIds: preset.authorIds + })) + ] as Section[]; + + return + + + Change Decoration + + + + + { + if (typeof item === "string") { + switch (item) { + case "none": + return setTryingDecoration(null)} + />; + case "create": + return + {tooltipProps => { }} + />} + ; + } + } else { + return + {tooltipProps => ( + setTryingDecoration(item) : () => { }} + isSelected={activeSelectedDecoration?.hash === item.hash} + decoration={item} + /> + )} + ; + } + }} + getItemKey={item => typeof item === "string" ? item : item.hash} + getSectionKey={section => section.sectionKey} + renderSectionHeader={section => } + sections={data} + /> +
+ + {isActiveDecorationPreset && Part of the {activeDecorationPreset.name} Preset} + {typeof activeSelectedDecoration === "object" && + + {activeSelectedDecoration?.alt} + + } + {activeDecorationHasAuthor && Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}} +
+
+ +
+ + +
+
+ + + {tooltipProps => } + +
+
+
; +} + +export const openChangeDecorationModal = () => + requireAvatarDecorationModal().then(() => openModal(props => )); diff --git a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx new file mode 100644 index 00000000..a5937b0d --- /dev/null +++ b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx @@ -0,0 +1,163 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Link } from "@components/Link"; +import { openInviteModal } from "@utils/discord"; +import { Margins } from "@utils/margins"; +import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common"; + +import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants"; +import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; +import { cl, requireAvatarDecorationModal, requireCreateStickerModal } from "../"; +import { AvatarDecorationModalPreview } from "../components"; + + +const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); + +const FileUpload = findComponentByCodeLazy("fileUploadInput,"); + +function useObjectURL(object: Blob | MediaSource | null) { + const [url, setUrl] = useState(null); + + useEffect(() => { + if (!object) return; + + const objectUrl = URL.createObjectURL(object); + setUrl(objectUrl); + + return () => { + URL.revokeObjectURL(objectUrl); + setUrl(null); + }; + }, [object]); + + return url; +} + +export default function CreateDecorationModal(props) { + const [name, setName] = useState(""); + const [file, setFile] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (error) setError(null); + }, [file]); + + const { create: createDecoration } = useCurrentUserDecorationsStore(); + + const fileUrl = useObjectURL(file); + + const decoration = useMemo(() => fileUrl ? { asset: fileUrl, skuId: RAW_SKU_ID } : null, [fileUrl]); + + return + + + Create Decoration + + + + +
+
+ {error !== null && {error.message}} + + + + File should be APNG or PNG. + + + + + + This name will be used when referring to this decoration. + + +
+
+ +
+
+ + Make sure your decoration does not violate + the guidelines + before creating your decoration. +
You can receive updates on your decoration's review by joining { + e.preventDefault(); + if (!GuildStore.getGuild(GUILD_ID)) { + const inviteAccepted = await openInviteModal(INVITE_KEY); + if (inviteAccepted) { + closeAllModals(); + FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + } + } else { + closeAllModals(); + FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + NavigationRouter.transitionToGuild(GUILD_ID); + } + }} + > + Decor's Discord server + . +
+
+ + + + +
; +} + +export const openCreateDecorationModal = () => + Promise.all([requireAvatarDecorationModal(), requireCreateStickerModal()]) + .then(() => openModal(props => )); diff --git a/src/plugins/decor/ui/styles.css b/src/plugins/decor/ui/styles.css new file mode 100644 index 00000000..ff10c82f --- /dev/null +++ b/src/plugins/decor/ui/styles.css @@ -0,0 +1,80 @@ +.vc-decor-danger-btn { + color: var(--white-500); + background-color: var(--button-danger-background); +} + +.vc-decor-change-decoration-modal-content { + position: relative; + display: flex; + border-radius: 5px 5px 0 0; + padding: 0 16px; + gap: 4px +} + +.vc-decor-change-decoration-modal-preview { + display: flex; + flex-direction: column; + margin-top: 24px; + gap: 8px; + max-width: 280px; +} + +.vc-decor-change-decoration-modal-decoration { + width: 80px; + height: 80px; +} + +.vc-decor-change-decoration-modal-footer { + justify-content: space-between; +} + +.vc-decor-change-decoration-modal-footer-btn-container { + display: flex; + flex-direction: row-reverse; +} + +.vc-decor-create-decoration-modal-content { + display: flex; + flex-direction: column; + gap: 20px; + padding: 0 16px; +} + +.vc-decor-create-decoration-modal-form-preview-container { + display: flex; + gap: 16px; +} + +.vc-decor-modal-header { + padding: 16px; +} + +.vc-decor-modal-footer { + padding: 16px; +} + +.vc-decor-create-decoration-modal-form { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: 16px; +} + +.vc-decor-sectioned-grid-list-container { + display: flex; + flex-direction: column; + overflow: hidden scroll; + max-height: 512px; + width: 352px; /* ((80 + 8 (grid gap)) * desired columns) (scrolled takes the extra 8 padding off conveniently) */ + gap: 12px; +} + +.vc-decor-sectioned-grid-list-grid { + display: flex; + flex-wrap: wrap; + gap: 8px +} + +.vc-decor-section-remove-margin { + margin-bottom: 0; +} diff --git a/src/utils/cloud.tsx b/src/utils/cloud.tsx index 02930622..f56c78dc 100644 --- a/src/utils/cloud.tsx +++ b/src/utils/cloud.tsx @@ -19,8 +19,7 @@ import * as DataStore from "@api/DataStore"; import { showNotification } from "@api/Notifications"; import { Settings } from "@api/Settings"; -import { findByProps } from "@webpack"; -import { UserStore } from "@webpack/common"; +import { OAuth2AuthorizeModal, UserStore } from "@webpack/common"; import { Logger } from "./Logger"; import { openModal } from "./modal"; @@ -91,8 +90,6 @@ export async function authorizeCloud() { return; } - const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal"); - openModal((props: any) => (r => { + let onClose: () => void, onAccept: () => void; + let inviteAccepted = false; + + FluxDispatcher.subscribe("INVITE_ACCEPT", onAccept = () => { + inviteAccepted = true; + }); + + FluxDispatcher.subscribe("INVITE_MODAL_CLOSE", onClose = () => { + FluxDispatcher.unsubscribe("INVITE_MODAL_CLOSE", onClose); + FluxDispatcher.unsubscribe("INVITE_ACCEPT", onAccept); + r(inviteAccepted); + }); + }); } export function getCurrentChannel() { diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index e44b1c9f..d7bb5d75 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -17,7 +17,7 @@ */ // eslint-disable-next-line path-alias/no-relative -import { filters, waitFor } from "@webpack"; +import { filters, findByPropsLazy, waitFor } from "@webpack"; import { waitForComponent } from "./internal"; import * as t from "./types/components"; @@ -55,6 +55,8 @@ export const MaskedLink = waitForComponent("MaskedLink", m => m?.t export const Timestamp = waitForComponent("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const Flex = waitForComponent("Flex", ["Justify", "Align", "Wrap"]); +export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal"); + waitFor(["FormItem", "Button"], m => { ({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar } = m); Forms = m; diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index 5d5424fe..b9bc434c 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -126,6 +126,7 @@ export type Button = ComponentType; focusProps?: any; + submitting?: boolean; submittingStartedLabel?: string; submittingFinishedLabel?: string; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index cef4d51d..f5d2a966 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -19,7 +19,7 @@ import type { Channel, User } from "discord-types/general"; // eslint-disable-next-line path-alias/no-relative -import { _resolveReady, findByPropsLazy, findLazy, waitFor } from "../webpack"; +import { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, waitFor } from "../webpack"; import type * as t from "./types/utils"; export let FluxDispatcher: t.FluxDispatcher; @@ -127,5 +127,9 @@ export const NavigationRouter: t.NavigationRouter = findByPropsLazy("transitionT export let SettingsRouter: any; waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m); -const { Permissions } = findLazy(m => typeof m.Permissions?.ADMINISTRATOR === "bigint") as { Permissions: t.PermissionsBits; }; -export { Permissions as PermissionsBits }; +export const { Permissions: PermissionsBits } = findLazy(m => typeof m.Permissions?.ADMINISTRATOR === "bigint") as { Permissions: t.PermissionsBits; }; + +export const zustandCreate: typeof import("zustand").default = findByCodeLazy("will be removed in v4"); + +const persistFilter = filters.byCode("[zustand persist middleware]"); +export const { persist: zustandPersist }: typeof import("zustand/middleware") = findLazy(m => m.persist && persistFilter(m.persist)); diff --git a/tsconfig.json b/tsconfig.json index db540745..4563f3f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, + "skipLibCheck": true, "lib": [ "DOM", "DOM.Iterable", From 66dbe7ef07c69f8c9963c947c5f1eac03b0e1222 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 30 Nov 2023 02:26:18 -0300 Subject: [PATCH 7/9] Fix reporter testing for extractAndLoadChunks --- scripts/generateReport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 719a8456..c1a4f711 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -410,7 +410,7 @@ function runTime(token: string) { const [code, matcher] = args; const module = Vencord.Webpack.findModuleFactory(...code); - if (module) result = module.toString().match(matcher); + if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher)); } else { // @ts-ignore result = Vencord.Webpack[method](...args); From 8e1546be000748ac647d5a1950e9c5750a016b27 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 30 Nov 2023 02:38:12 -0300 Subject: [PATCH 8/9] Include ignored Discord errors in summary --- scripts/generateReport.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index c1a4f711..a75a5985 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -105,7 +105,14 @@ async function printReport() { console.log(); - report.otherErrors = report.otherErrors.filter(e => !IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))); + const ignoredErrors = [] as string[]; + report.otherErrors = report.otherErrors.filter(e => { + if (IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))) { + ignoredErrors.push(e); + return false; + } + return true; + }); console.log("## Discord Errors"); report.otherErrors.forEach(e => { @@ -114,6 +121,13 @@ async function printReport() { console.log(); + console.log("## Ignored Discord Errors"); + ignoredErrors.forEach(e => { + console.log(`- ${toCodeBlock(e)}`); + }); + + console.log(); + if (process.env.DISCORD_WEBHOOK) { await fetch(process.env.DISCORD_WEBHOOK, { method: "POST", From fccdd3dc08fb45fb9dad79d2dc8a0ac5ddb120f9 Mon Sep 17 00:00:00 2001 From: V Date: Thu, 30 Nov 2023 17:28:53 +0100 Subject: [PATCH 9/9] migrate to new badge api we used to store badges on the discord cdn. since discord is now making it harder to use their cdn for such purposes (due to expiring links), we are forced to stop using it thus, badges are now stored on our server, accessible via https://badges.vencord.dev. The full list of badges is now at https://badges.vencord.dev/badges.json --- src/plugins/_api/badges.tsx | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/plugins/_api/badges.tsx b/src/plugins/_api/badges.tsx index 11e843db..16b244a1 100644 --- a/src/plugins/_api/badges.tsx +++ b/src/plugins/_api/badges.tsx @@ -22,14 +22,13 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { Heart } from "@components/Heart"; import { Devs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { isPluginDev } from "@utils/misc"; import { closeModal, Modals, openModal } from "@utils/modal"; import definePlugin from "@utils/types"; import { Forms, Toasts } from "@webpack/common"; -const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png"; +const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png"; const ContributorBadge: ProfileBadge = { description: "Vencord Contributor", @@ -45,7 +44,7 @@ const ContributorBadge: ProfileBadge = { link: "https://github.com/Vendicated/Vencord" }; -let DonorBadges = {} as Record[]>; +let DonorBadges = {} as Record>>; async function loadBadges(noCache = false) { DonorBadges = {}; @@ -54,19 +53,8 @@ async function loadBadges(noCache = false) { if (noCache) init.cache = "no-cache"; - const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv", init) - .then(r => r.text()); - - const lines = badges.trim().split("\n"); - if (lines.shift() !== "id,tooltip,image") { - new Logger("BadgeAPI").error("Invalid badges.csv file!"); - return; - } - - for (const line of lines) { - const [id, description, image] = line.split(","); - (DonorBadges[id] ??= []).push({ image, description }); - } + DonorBadges = await fetch("https://badges.vencord.dev/badges.json", init) + .then(r => r.json()); } export default definePlugin({ @@ -127,7 +115,8 @@ export default definePlugin({ getDonorBadges(userId: string) { return DonorBadges[userId]?.map(badge => ({ - ...badge, + image: badge.badge, + description: badge.tooltip, position: BadgePosition.START, props: { style: {