From 30bc979c8ddcb0da04fd494fdf2b8631183e3731 Mon Sep 17 00:00:00 2001 From: jd <28711824+jewdev@users.noreply.github.com> Date: Sun, 31 Dec 2023 02:06:19 +0200 Subject: [PATCH 01/50] feat(Urban Dictionary): Chooses top rated definition & more results. (#2080) --- src/plugins/urbanDictionary/README.md | 13 +++++++++++++ src/plugins/urbanDictionary/index.ts | 22 ++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/plugins/urbanDictionary/README.md diff --git a/src/plugins/urbanDictionary/README.md b/src/plugins/urbanDictionary/README.md new file mode 100644 index 000000000..e065456a3 --- /dev/null +++ b/src/plugins/urbanDictionary/README.md @@ -0,0 +1,13 @@ +# Urban Dictionary + +Use /urban slash command to search for a definition for a word on [Urban Dictionary](https://www.urbandictionary.com/). + +## Preview + +![preview](https://i.imgur.com/1zwzj38.png) + +## Usage + +- Enable this plugin +- Set plugin settings as desired +- Type /urban and start getting definitions right into your Discord client. diff --git a/src/plugins/urbanDictionary/index.ts b/src/plugins/urbanDictionary/index.ts index 840fe5cb6..89dcdcba4 100644 --- a/src/plugins/urbanDictionary/index.ts +++ b/src/plugins/urbanDictionary/index.ts @@ -18,14 +18,24 @@ import { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands"; import { ApplicationCommandInputType } from "@api/Commands/types"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; + +const settings = definePluginSettings({ + resultsAmount: { + type: OptionType.NUMBER, + description: "The amount of results you want to get (more gives better results, but is slower)", + default: 10 + } +}); export default definePlugin({ name: "UrbanDictionary", description: "Search for a word on Urban Dictionary via /urban slash command", authors: [Devs.jewdev], dependencies: ["CommandsAPI"], + settings, commands: [ { name: "urban", @@ -41,12 +51,16 @@ export default definePlugin({ ], execute: async (args, ctx) => { try { - const query = encodeURIComponent(args[0].value); - const { list: [definition] } = await (await fetch(`https://api.urbandictionary.com/v0/define?term=${query}`)).json(); + const query: string = encodeURIComponent(args[0].value); + const { list } = await fetch(`https://api.urbandictionary.com/v0/define?term=${query}&per_page=${settings.store.resultsAmount}`).then(response => response.json()); - if (!definition) + if (!list.length) return void sendBotMessage(ctx.channel.id, { content: "No results found." }); + const definition = list.reduce((prev, curr) => { + return prev.thumbs_up > curr.thumbs_up ? prev : curr; + }); + const linkify = (text: string) => text .replaceAll("\r\n", "\n") .replace(/([*>_`~\\])/gsi, "\\$1") From 7e395fc6968aced5d45fae55db646def1d555c49 Mon Sep 17 00:00:00 2001 From: V Date: Sun, 31 Dec 2023 05:02:05 +0100 Subject: [PATCH 02/50] Update codeburger mirror --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8611babd7..a43c9f834 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Vencord -[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Ven/cord&color=2185D0&logo=data:image/png;base64,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)](https://codeberg.org/Ven/cord) +[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,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)](https://codeberg.org/Vee/cord) The cutest Discord client mod From d0dfdbbd5fc263ab105fb9d9f1c2f1295a4c8c7e Mon Sep 17 00:00:00 2001 From: Lewis Crichton Date: Thu, 28 Dec 2023 02:02:49 +0000 Subject: [PATCH 03/50] fix: Vencord_cloudSecret check (#2077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit finally got around to fixing it - `null` is never a valid return value, it's `undefined` 🤦 --- src/Vencord.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Vencord.ts b/src/Vencord.ts index a106a0b7d..29e965fa0 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -44,7 +44,7 @@ async function syncSettings() { // pre-check for local shared settings if ( Settings.cloud.authenticated && - await dsGet("Vencord_cloudSecret") === null // this has been enabled due to local settings share or some other bug + !await dsGet("Vencord_cloudSecret") // this has been enabled due to local settings share or some other bug ) { // show a notification letting them know and tell them how to fix it showNotification({ @@ -145,4 +145,3 @@ document.addEventListener("DOMContentLoaded", () => { })); } }, { once: true }); - From a963a19bdc67400a024253fd8285bf7677be7db1 Mon Sep 17 00:00:00 2001 From: Jack <30497388+FieryFlames@users.noreply.github.com> Date: Wed, 3 Jan 2024 07:37:56 -0500 Subject: [PATCH 04/50] fix(Decor): Fix AvatarDecorationModalPreview find (#2089) --- src/plugins/decor/ui/components/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/decor/ui/components/index.ts b/src/plugins/decor/ui/components/index.ts index 8f39a10ee..8fe41fc92 100644 --- a/src/plugins/decor/ui/components/index.ts +++ b/src/plugins/decor/ui/components/index.ts @@ -19,7 +19,7 @@ export let DecorationGridItem: DecorationGridItemComponent; export const setDecorationGridItem = v => DecorationGridItem = v; export const AvatarDecorationModalPreview = LazyComponentWebpack(() => { - const component = findComponentByCode("AvatarDecorationModalPreview"); + const component = findComponentByCode(".shopPreviewBanner"); return React.memo(component); }); From 1eb2510353cf3ffac9eb746aba8e08d01a54ac06 Mon Sep 17 00:00:00 2001 From: nexpid <60316309+nexpid@users.noreply.github.com> Date: Wed, 3 Jan 2024 13:49:03 +0100 Subject: [PATCH 05/50] feat(Decor): Enforce guidelines more (#2035) --- src/plugins/decor/index.tsx | 31 +---- src/plugins/decor/settings.tsx | 47 +++++++ .../decor/ui/modals/ChangeDecorationModal.tsx | 110 ++++++++-------- .../decor/ui/modals/CreateDecorationModal.tsx | 121 +++++++++--------- .../decor/ui/modals/GuidelinesModal.tsx | 68 ++++++++++ src/plugins/decor/ui/styles.css | 4 +- 6 files changed, 242 insertions(+), 139 deletions(-) create mode 100644 src/plugins/decor/settings.tsx create mode 100644 src/plugins/decor/ui/modals/GuidelinesModal.tsx diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 4dd7aa0c9..ce546d309 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -6,21 +6,17 @@ import "./ui/styles.css"; -import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import { classes } from "@utils/misc"; -import { closeAllModals } from "@utils/modal"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { FluxDispatcher, Forms, UserStore } from "@webpack/common"; +import { UserStore } from "@webpack/common"; import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants"; import { useAuthorizationStore } from "./lib/stores/AuthorizationStore"; import { useCurrentUserDecorationsStore } from "./lib/stores/CurrentUserDecorationsStore"; import { useUserDecorAvatarDecoration, useUsersDecorationsStore } from "./lib/stores/UsersDecorationsStore"; +import { settings } from "./settings"; import { setDecorationGridDecoration, setDecorationGridItem } from "./ui/components"; import DecorSection from "./ui/components/DecorSection"; @@ -30,27 +26,6 @@ export interface AvatarDecoration { skuId: string; } -const settings = definePluginSettings({ - changeDecoration: { - type: OptionType.COMPONENT, - description: "Change your avatar decoration", - component() { - return
- - - You can also access Decor decorations from the { - e.preventDefault(); - closeAllModals(); - FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" }); - }} - >Profiles page. - -
; - } - } -}); export default definePlugin({ name: "Decor", description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.", diff --git a/src/plugins/decor/settings.tsx b/src/plugins/decor/settings.tsx new file mode 100644 index 000000000..0d3628cc6 --- /dev/null +++ b/src/plugins/decor/settings.tsx @@ -0,0 +1,47 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Link } from "@components/Link"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { closeAllModals } from "@utils/modal"; +import { OptionType } from "@utils/types"; +import { FluxDispatcher, Forms } from "@webpack/common"; + +import DecorSection from "./ui/components/DecorSection"; + +export const settings = definePluginSettings({ + changeDecoration: { + type: OptionType.COMPONENT, + description: "Change your avatar decoration", + component() { + if (!Vencord.Plugins.plugins.Decor.started) return + Enable Decor and restart your client to change your avatar decoration. + ; + + return
+ + + You can also access Decor decorations from the { + e.preventDefault(); + closeAllModals(); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" }); + }} + >Profiles page. + +
; + } + }, + agreedToGuidelines: { + type: OptionType.BOOLEAN, + description: "Agreed to guidelines", + hidden: true, + default: false + } +}); diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx index bed007174..f2a482818 100644 --- a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx @@ -4,11 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; -import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common"; import { User } from "discord-types/general"; @@ -18,6 +19,7 @@ import { GUILD_ID, INVITE_KEY } from "../../lib/constants"; import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; import { decorationToAvatarDecoration } from "../../lib/utils/decoration"; +import { settings } from "../../settings"; import { cl, requireAvatarDecorationModal } from "../"; import { AvatarDecorationModalPreview } from "../components"; import DecorationGridCreate from "../components/DecorationGridCreate"; @@ -25,6 +27,7 @@ import DecorationGridNone from "../components/DecorationGridNone"; import DecorDecorationGridDecoration from "../components/DecorDecorationGridDecoration"; import SectionedGridList from "../components/SectionedGridList"; import { openCreateDecorationModal } from "./CreateDecorationModal"; +import { openGuidelinesModal } from "./GuidelinesModal"; const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); @@ -83,7 +86,7 @@ function SectionHeader({ section }: { section: Section; }) { ; } -export default function ChangeDecorationModal(props: any) { +function ChangeDecorationModal(props: ModalProps) { // undefined = not trying, null = none, Decoration = selected const [tryingDecoration, setTryingDecoration] = useState(undefined); const isTryingDecoration = typeof tryingDecoration !== "undefined"; @@ -116,6 +119,7 @@ export default function ChangeDecorationModal(props: any) { const data = [ { title: "Your Decorations", + subtitle: "You can delete your own decorations by right clicking on them.", sectionKey: "ownDecorations", items: ["none", ...ownDecorations, "create"] }, @@ -148,60 +152,62 @@ export default function ChangeDecorationModal(props: any) { className={cl("change-decoration-modal-content")} scrollbarType="none" > - { - if (typeof item === "string") { - switch (item) { - case "none": - return setTryingDecoration(null)} - />; - case "create": - return - {tooltipProps => + { + if (typeof item === "string") { + switch (item) { + case "none": + return setTryingDecoration(null)} + />; + case "create": + return + {tooltipProps => { }} + />} + ; + } + } else { + return + {tooltipProps => ( + { }} - />} - ; + className={cl("change-decoration-modal-decoration")} + onSelect={item.reviewed !== false ? () => setTryingDecoration(item) : () => { }} + isSelected={activeSelectedDecoration?.hash === item.hash} + decoration={item} + /> + )} + ; } - } else { - return - {tooltipProps => ( - setTryingDecoration(item) : () => { }} - isSelected={activeSelectedDecoration?.hash === item.hash} - decoration={item} - /> - )} - ; - } - }} - getItemKey={item => typeof item === "string" ? item : item.hash} - getSectionKey={section => section.sectionKey} - renderSectionHeader={section => } - sections={data} - /> -
- typeof item === "string" ? item : item.hash} + getSectionKey={section => section.sectionKey} + renderSectionHeader={section => } + sections={data} /> - {isActiveDecorationPreset && Part of the {activeDecorationPreset.name} Preset} - {typeof activeSelectedDecoration === "object" && - - {activeSelectedDecoration?.alt} - - } - {activeDecorationHasAuthor && Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}} -
+
+ + {isActiveDecorationPreset && Part of the {activeDecorationPreset.name} Preset} + {typeof activeSelectedDecoration === "object" && + + {activeSelectedDecoration?.alt} + + } + {activeDecorationHasAuthor && Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}} +
+
diff --git a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx index a5937b0dd..eea79d86e 100644 --- a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx @@ -4,10 +4,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import ErrorBoundary from "@components/ErrorBoundary"; import { Link } from "@components/Link"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; -import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common"; @@ -21,6 +22,8 @@ const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); const FileUpload = findComponentByCodeLazy("fileUploadInput,"); +const { default: HelpMessage, HelpMessageTypes } = findByPropsLazy("HelpMessageTypes"); + function useObjectURL(object: Blob | MediaSource | null) { const [url, setUrl] = useState(null); @@ -39,7 +42,7 @@ function useObjectURL(object: Blob | MediaSource | null) { return url; } -export default function CreateDecorationModal(props) { +function CreateDecorationModal(props: ModalProps) { const [name, setName] = useState(""); const [file, setFile] = useState(null); const [submitting, setSubmitting] = useState(false); @@ -75,65 +78,69 @@ export default function CreateDecorationModal(props) { className={cl("create-decoration-modal-content")} scrollbarType="none" > -
-
- {error !== null && {error.message}} - - + + Make sure your decoration does not violate + the guidelines + before submitting it. + +
+
+ {error !== null && {error.message}} + + + + File should be APNG or PNG. + + + + + + This name will be used when referring to this decoration. + + +
+
+ - - File should be APNG or PNG. - - - - - - This name will be used when referring to this decoration. - - +
-
- -
-
- - Make sure your decoration does not violate - the guidelines - before creating your decoration. -
You can receive updates on your decoration's review by joining { - e.preventDefault(); - if (!GuildStore.getGuild(GUILD_ID)) { - const inviteAccepted = await openInviteModal(INVITE_KEY); - if (inviteAccepted) { + +
You can receive updates on your decoration's review by joining { + e.preventDefault(); + if (!GuildStore.getGuild(GUILD_ID)) { + const inviteAccepted = await openInviteModal(INVITE_KEY); + if (inviteAccepted) { + closeAllModals(); + FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + } + } else { closeAllModals(); FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + NavigationRouter.transitionToGuild(GUILD_ID); } - } else { - closeAllModals(); - FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); - NavigationRouter.transitionToGuild(GUILD_ID); - } - }} - > - Decor's Discord server - . -
+ }} + > + Decor's Discord server + . +
+ + + + ; +} + +export const openGuidelinesModal = () => + requireAvatarDecorationModal().then(() => openModal(props => )); diff --git a/src/plugins/decor/ui/styles.css b/src/plugins/decor/ui/styles.css index ff10c82fa..9730efb7e 100644 --- a/src/plugins/decor/ui/styles.css +++ b/src/plugins/decor/ui/styles.css @@ -8,7 +8,7 @@ display: flex; border-radius: 5px 5px 0 0; padding: 0 16px; - gap: 4px + gap: 4px; } .vc-decor-change-decoration-modal-preview { @@ -72,7 +72,7 @@ .vc-decor-sectioned-grid-list-grid { display: flex; flex-wrap: wrap; - gap: 8px + gap: 8px; } .vc-decor-section-remove-margin { From 4a1c85c8adc04e4874b73b32924d24bd0839aab9 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 3 Jan 2024 09:58:09 -0300 Subject: [PATCH 06/50] Decor: only search for DecorationModalStyles once --- src/plugins/decor/ui/index.ts | 3 ++- src/plugins/decor/ui/modals/ChangeDecorationModal.tsx | 5 ++--- src/plugins/decor/ui/modals/CreateDecorationModal.tsx | 5 +---- src/plugins/decor/ui/modals/GuidelinesModal.tsx | 5 +---- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/plugins/decor/ui/index.ts b/src/plugins/decor/ui/index.ts index 52b169d77..0ead602e2 100644 --- a/src/plugins/decor/ui/index.ts +++ b/src/plugins/decor/ui/index.ts @@ -5,9 +5,10 @@ */ import { classNameFactory } from "@api/Styles"; -import { extractAndLoadChunksLazy } from "@webpack"; +import { extractAndLoadChunksLazy, findByPropsLazy } from "@webpack"; export const cl = classNameFactory("vc-decor-"); +export const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); export const requireAvatarDecorationModal = extractAndLoadChunksLazy(["openAvatarDecorationModal:"]); export const requireCreateStickerModal = extractAndLoadChunksLazy(["stickerInspected]:"]); diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx index f2a482818..5fbe165ce 100644 --- a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx @@ -10,7 +10,7 @@ import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { findComponentByCodeLazy } from "@webpack"; import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common"; import { User } from "discord-types/general"; @@ -20,7 +20,7 @@ import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; import { decorationToAvatarDecoration } from "../../lib/utils/decoration"; import { settings } from "../../settings"; -import { cl, requireAvatarDecorationModal } from "../"; +import { cl, DecorationModalStyles, requireAvatarDecorationModal } from "../"; import { AvatarDecorationModalPreview } from "../components"; import DecorationGridCreate from "../components/DecorationGridCreate"; import DecorationGridNone from "../components/DecorationGridNone"; @@ -30,7 +30,6 @@ import { openCreateDecorationModal } from "./CreateDecorationModal"; import { openGuidelinesModal } from "./GuidelinesModal"; const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); -const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); function usePresets() { const [presets, setPresets] = useState([]); diff --git a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx index eea79d86e..0dcf855ef 100644 --- a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx @@ -14,12 +14,9 @@ import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, Text import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; -import { cl, requireAvatarDecorationModal, requireCreateStickerModal } from "../"; +import { cl, DecorationModalStyles, requireAvatarDecorationModal, requireCreateStickerModal } from "../"; import { AvatarDecorationModalPreview } from "../components"; - -const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); - const FileUpload = findComponentByCodeLazy("fileUploadInput,"); const { default: HelpMessage, HelpMessageTypes } = findByPropsLazy("HelpMessageTypes"); diff --git a/src/plugins/decor/ui/modals/GuidelinesModal.tsx b/src/plugins/decor/ui/modals/GuidelinesModal.tsx index ab1bf441e..716aea6bd 100644 --- a/src/plugins/decor/ui/modals/GuidelinesModal.tsx +++ b/src/plugins/decor/ui/modals/GuidelinesModal.tsx @@ -6,15 +6,12 @@ import { Link } from "@components/Link"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { findByPropsLazy } from "@webpack"; import { Button, Forms, Text } from "@webpack/common"; import { settings } from "../../settings"; -import { cl, requireAvatarDecorationModal } from "../"; +import { cl, DecorationModalStyles, requireAvatarDecorationModal } from "../"; import { openCreateDecorationModal } from "./CreateDecorationModal"; -const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); - function GuidelinesModal(props: ModalProps) { return Date: Fri, 5 Jan 2024 22:07:00 +0100 Subject: [PATCH 07/50] fix cobu sync --- .github/workflows/codeberg-mirror.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeberg-mirror.yml b/.github/workflows/codeberg-mirror.yml index 647a43135..1b2266ee7 100644 --- a/.github/workflows/codeberg-mirror.yml +++ b/.github/workflows/codeberg-mirror.yml @@ -18,5 +18,5 @@ jobs: fetch-depth: 0 - uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1 with: - target_repo_url: "git@codeberg.org:Ven/cord.git" + target_repo_url: "git@codeberg.org:Vee/cord.git" ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }} From 88fc15752d95881f2ba90d935c7dc5404649c806 Mon Sep 17 00:00:00 2001 From: sunnie <78964224+sunnniee@users.noreply.github.com> Date: Fri, 5 Jan 2024 23:12:13 +0200 Subject: [PATCH 08/50] webContextMenus: add Show My Camera item (#2086) --- src/plugins/webContextMenus.web/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index eb076dfd0..50a1b90a7 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -169,6 +169,15 @@ export default definePlugin({ match: /let\{text:\i=""/, replace: "return [null,null];$&" } + }, + + // Add back "Show My Camera" context menu + { + find: '.default("MediaEngineWebRTC");', + replacement: { + match: /supports\(\i\)\{switch\(\i\)\{case (\i).Features/, + replace: "$&.DISABLE_VIDEO:return true;case $1.Features" + } } ], From 3eada99ad623ae783a39eb2c8238ac66ed371132 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 5 Jan 2024 22:15:54 +0100 Subject: [PATCH 09/50] fix(ToolBox): don't add entries of disabled plugins --- src/plugins/vencordToolbox/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index 2b0ce14e6..0a805a0d2 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -33,7 +33,7 @@ function VencordPopout(onClose: () => void) { const pluginEntries = [] as ReactNode[]; for (const plugin of Object.values(Vencord.Plugins.plugins)) { - if (plugin.toolboxActions) { + if (plugin.toolboxActions && Vencord.Plugins.isPluginEnabled(plugin.name)) { pluginEntries.push( Date: Fri, 5 Jan 2024 23:31:09 +0100 Subject: [PATCH 10/50] fix wrongly patching 'Events' context menu --- src/plugins/biggerStreamPreview/index.tsx | 2 +- src/plugins/copyUserURLs/index.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/biggerStreamPreview/index.tsx b/src/plugins/biggerStreamPreview/index.tsx index acad564da..40bbe30a8 100644 --- a/src/plugins/biggerStreamPreview/index.tsx +++ b/src/plugins/biggerStreamPreview/index.tsx @@ -82,7 +82,7 @@ export const streamContextPatch: NavContextMenuPatchCallback = (children, { stre }; export const userContextPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => { - return addViewStreamContext(children, { userId: user.id }); + if (user) return addViewStreamContext(children, { userId: user.id }); }; export default definePlugin({ diff --git a/src/plugins/copyUserURLs/index.tsx b/src/plugins/copyUserURLs/index.tsx index e3c336fb2..9f69674cf 100644 --- a/src/plugins/copyUserURLs/index.tsx +++ b/src/plugins/copyUserURLs/index.tsx @@ -30,6 +30,8 @@ interface UserContextProps { } const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => { + if (!user) return; + children.push( Date: Fri, 5 Jan 2024 23:51:49 +0100 Subject: [PATCH 11/50] ReverseImageSearch: add support for image modal --- src/plugins/reverseImageSearch/index.tsx | 116 ++++++++++++----------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/src/plugins/reverseImageSearch/index.tsx b/src/plugins/reverseImageSearch/index.tsx index 811e7ff08..6c5f3e729 100644 --- a/src/plugins/reverseImageSearch/index.tsx +++ b/src/plugins/reverseImageSearch/index.tsx @@ -36,62 +36,68 @@ function search(src: string, engine: string) { open(engine + encodeURIComponent(src), "_blank"); } -const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (!props) return; - const { reverseImageSearchType, itemHref, itemSrc } = props; +function makeSearchItem(src: string) { + return ( + + {Object.keys(Engines).map((engine, i) => { + const key = "search-image-" + engine; + return ( + + = 3 // Do not round Google, Yandex & SauceNAO + ? "50%" + : void 0 + }} + aria-hidden="true" + height={16} + width={16} + src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")} + /> + {engine} + + } + action={() => search(src, Engines[engine])} + /> + ); + })} + + + All + + } + action={() => Object.values(Engines).forEach(e => search(src, e))} + /> + + ); +} - if (!reverseImageSearchType || reverseImageSearchType !== "img") return; +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (props?.reverseImageSearchType !== "img") return; - const src = itemHref ?? itemSrc; + const src = props.itemHref ?? props.itemSrc; const group = findGroupChildrenByChildId("copy-link", children); - if (group) { - group.push(( - - {Object.keys(Engines).map((engine, i) => { - const key = "search-image-" + engine; - return ( - - = 3 // Do not round Google, Yandex & SauceNAO - ? "50%" - : void 0 - }} - aria-hidden="true" - height={16} - width={16} - src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")} - /> - {engine} - - } - action={() => search(src, Engines[engine])} - /> - ); - })} - - - All - - } - action={() => Object.values(Engines).forEach(e => search(src, e))} - /> - - )); - } + group?.push(makeSearchItem(src)); +}; + +const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (!props?.src) return; + + const group = findGroupChildrenByChildId("copy-native-link", children) ?? children; + group.push(makeSearchItem(props.src)); }; export default definePlugin({ @@ -111,10 +117,12 @@ export default definePlugin({ ], start() { - addContextMenuPatch("message", imageContextMenuPatch); + addContextMenuPatch("message", messageContextMenuPatch); + addContextMenuPatch("image-context", imageContextMenuPatch); }, stop() { - removeContextMenuPatch("message", imageContextMenuPatch); + removeContextMenuPatch("message", messageContextMenuPatch); + removeContextMenuPatch("image-context", imageContextMenuPatch); } }); From 6530526fb2eff5881fe677c6437ba73cc31597b0 Mon Sep 17 00:00:00 2001 From: Moxie <124418090+moxie-coder@users.noreply.github.com> Date: Fri, 5 Jan 2024 18:10:16 -0500 Subject: [PATCH 12/50] Updater: Fix grammar (1 Updates => 1 Update) (#2084) Co-authored-by: V --- src/components/VencordSettings/UpdaterTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx index 81433960f..a221bb38b 100644 --- a/src/components/VencordSettings/UpdaterTab.tsx +++ b/src/components/VencordSettings/UpdaterTab.tsx @@ -113,7 +113,7 @@ function Updatable(props: CommonProps) { ) : ( - {isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"} + {isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"} )} From be9ec3b7ac79334e5c753d6377894496006f5fdf Mon Sep 17 00:00:00 2001 From: Grzesiek11 Date: Sat, 6 Jan 2024 00:23:09 +0100 Subject: [PATCH 13/50] SendTimestamps: add setting to disable message replacement (#2076) --- src/plugins/sendTimestamps/index.tsx | 19 ++++++++++++++++--- src/utils/constants.ts | 4 ++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx index 7904545c8..6d488add6 100644 --- a/src/plugins/sendTimestamps/index.tsx +++ b/src/plugins/sendTimestamps/index.tsx @@ -19,14 +19,23 @@ import "./styles.css"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; +import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Devs } from "@utils/constants"; import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { Button, ButtonLooks, ButtonWrapperClasses, Forms, Parser, Select, Tooltip, useMemo, useState } from "@webpack/common"; +const settings = definePluginSettings({ + replaceMessageContents: { + description: "Replace timestamps in message contents", + type: OptionType.BOOLEAN, + default: true, + }, +}); + function parseTime(time: string) { const cleanTime = time.slice(1, -1).replace(/(\d)(AM|PM)$/i, "$1 $2"); @@ -116,9 +125,11 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi export default definePlugin({ name: "SendTimestamps", description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!", - authors: [Devs.Ven, Devs.Tyler], + authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11], dependencies: ["MessageEventsAPI"], + settings: settings, + patches: [ { find: "ChannelTextAreaButtons", @@ -131,7 +142,9 @@ export default definePlugin({ start() { this.listener = addPreSendListener((_, msg) => { - msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); + if (settings.store.replaceMessageContents) { + msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); + } }); }, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 2962df06f..a94ba0fc3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -399,6 +399,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "maisy", id: 257109471589957632n, }, + Grzesiek11: { + name: "Grzesiek11", + id: 368475654662127616n, + }, } satisfies Record); // iife so #__PURE__ works correctly From d5c58ab2b3fa24b00ab310fa7152174f2d235267 Mon Sep 17 00:00:00 2001 From: Hari Rana Date: Fri, 5 Jan 2024 18:55:14 -0500 Subject: [PATCH 14/50] updater: Add top and bottom margins between commits (#2042) Co-authored-by: V --- src/components/VencordSettings/UpdaterTab.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx index a221bb38b..0a5d1f149 100644 --- a/src/components/VencordSettings/UpdaterTab.tsx +++ b/src/components/VencordSettings/UpdaterTab.tsx @@ -81,9 +81,12 @@ function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) { return ( - + {updates.map(({ hash, author, message }) => ( -
+
Date: Sat, 6 Jan 2024 01:02:09 +0100 Subject: [PATCH 15/50] TextReplace: allow replacing with nothing (#1933) --- src/plugins/textReplace/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index 5d66d2265..416ce83fb 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -213,7 +213,7 @@ function applyRules(content: string): string { if (stringRules) { for (const rule of stringRules) { - if (!rule.find || !rule.replace) continue; + if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, ""); @@ -222,7 +222,7 @@ function applyRules(content: string): string { if (regexRules) { for (const rule of regexRules) { - if (!rule.find || !rule.replace) continue; + if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; try { From d73790efb392b09371f0a0e7120098ee23e86a58 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 6 Jan 2024 01:22:04 +0100 Subject: [PATCH 16/50] bump to v1.6.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b59fbafe7..0b373008e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.5", + "version": "1.6.6", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From ba2695b499d511124fbc16e511ceab06c8ec1b78 Mon Sep 17 00:00:00 2001 From: Grzesiek11 Date: Sat, 6 Jan 2024 03:05:23 +0100 Subject: [PATCH 17/50] Add FixCodeblockGap plugin (#2064) --- src/plugins/fixCodeblockGap/index.ts | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/plugins/fixCodeblockGap/index.ts diff --git a/src/plugins/fixCodeblockGap/index.ts b/src/plugins/fixCodeblockGap/index.ts new file mode 100644 index 000000000..133409959 --- /dev/null +++ b/src/plugins/fixCodeblockGap/index.ts @@ -0,0 +1,35 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "FixCodeblockGap", + description: "Removes the gap between codeblocks and text below it", + authors: [Devs.Grzesiek11], + patches: [ + { + find: ".default.Messages.DELETED_ROLE_PLACEHOLDER", + replacement: { + match: String.raw`/^${"```"}(?:([a-z0-9_+\-.#]+?)\n)?\n*([^\n][^]*?)\n*${"```"}`, + replace: "$&\\n?", + }, + }, + ], +}); From ba6d23a31f61841e190e771b1ccc4d5d18e666db Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 9 Jan 2024 17:39:38 +0100 Subject: [PATCH 18/50] fix adding View Raw to undesired channel context menus --- src/plugins/permissionsViewer/index.tsx | 4 +++- src/plugins/viewRaw/index.tsx | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 985b23842..9e0131e64 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -126,7 +126,9 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback { return (children, props) => () => { - if (!props || (type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild)) return children; + if (!props) return; + if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild))) + return children; const group = findGroupChildrenByChildId(childId, children); diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index f516b5d7a..187dbc4e3 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -117,22 +117,26 @@ const settings = definePluginSettings({ } }); -function MakeContextCallback(name: string) { +function MakeContextCallback(name: "Guild" | "User" | "Channel") { const callback: NavContextMenuPatchCallback = (children, props) => () => { - if ((name === "Guild" && !props.guild) || (name === "User" && !props.user)) return; + const value = props[name.toLowerCase()]; + if (!value) return; + if (props.label === "Channel Actions") return; // random shit like notification settings + const lastChild = children.at(-1); if (lastChild?.key === "developer-actions") { const p = lastChild.props; if (!Array.isArray(p.children)) p.children = [p.children]; - ({ children } = p); + + children = p.children; } children.splice(-1, 0, openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)} + action={() => openViewRawModal(JSON.stringify(value, null, 4), name)} icon={CopyIcon} /> ); From 8c890028672be7623949ab1aa45bc5796c773a48 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 9 Jan 2024 17:44:18 +0100 Subject: [PATCH 19/50] forgor not everyone uses enlgihs --- src/plugins/viewRaw/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 187dbc4e3..08acdc4c5 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -27,7 +27,7 @@ import { Margins } from "@utils/margins"; import { copyWithToast } from "@utils/misc"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common"; +import { Button, ChannelStore, Forms, i18n, Menu, Text } from "@webpack/common"; import { Message } from "discord-types/general"; @@ -121,7 +121,7 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel") { const callback: NavContextMenuPatchCallback = (children, props) => () => { const value = props[name.toLowerCase()]; if (!value) return; - if (props.label === "Channel Actions") return; // random shit like notification settings + if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings const lastChild = children.at(-1); if (lastChild?.key === "developer-actions") { From cb7045c00bb7bbeba4b70d3a1806069878a456f3 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 13 Jan 2024 19:05:01 +0100 Subject: [PATCH 20/50] WebContextMenus: use vesktop native clipboard - fixes some permission issues --- src/plugins/webContextMenus.web/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index 50a1b90a7..bb98c61d7 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -182,6 +182,12 @@ export default definePlugin({ ], async copyImage(url: string) { + if (IS_VESKTOP && VesktopNative.clipboard) { + const data = await fetch(url).then(r => r.arrayBuffer()); + VesktopNative.clipboard.copyImage(data, url); + return; + } + // Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png // via canvas first const img = new Image(); From 69a4d2734efcfd4e1ceed18b61e56d91ec446876 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 13 Jan 2024 19:08:11 +0100 Subject: [PATCH 21/50] fix(git updater): correctly persist isDev switch --- src/main/updater/git.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts index e96026443..2ff3ba512 100644 --- a/src/main/updater/git.ts +++ b/src/main/updater/git.ts @@ -73,6 +73,8 @@ async function build() { const command = isFlatpak ? "flatpak-spawn" : "node"; const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"]; + if (IS_DEV) args.push("--dev"); + const res = await execFile(command, args, opts); return !res.stderr.includes("Build failed"); From 2ab1c50c7355849e42ad81afcfef3d008b459247 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 13 Jan 2024 19:27:41 +0100 Subject: [PATCH 22/50] QuickCss: reopen existing window instead of new one --- src/main/ipcMain.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 6254bc826..47d400eb6 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -139,8 +139,15 @@ export function initIpc(mainWindow: BrowserWindow) { } ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { + const title = "Vencord QuickCSS Editor"; + const existingWindow = BrowserWindow.getAllWindows().find(w => w.title === title); + if (existingWindow && !existingWindow.isDestroyed()) { + existingWindow.focus(); + return; + } + const win = new BrowserWindow({ - title: "Vencord QuickCSS Editor", + title, autoHideMenuBar: true, darkTheme: true, webPreferences: { From 8bd54173dbb1af446e1d710dd65dd3d67f68fc0e Mon Sep 17 00:00:00 2001 From: Manti <67705577+mantikafasi@users.noreply.github.com> Date: Mon, 15 Jan 2024 00:46:24 +0300 Subject: [PATCH 23/50] Bring back ReviewDB (#2097) Changes from old version: - You can now delete reviews on your own profile - You can now block up to 50 users. This will prevent them from leaving reviews on your profile Co-authored-by: V --- src/plugins/reviewDB/auth.tsx | 78 +++++++ .../reviewDB/components/BlockedUserModal.tsx | 99 +++++++++ .../reviewDB/components/MessageButton.tsx | 85 ++++++++ .../reviewDB/components/ReviewBadge.tsx | 50 +++++ .../reviewDB/components/ReviewComponent.tsx | 188 ++++++++++++++++ .../reviewDB/components/ReviewModal.tsx | 105 +++++++++ .../reviewDB/components/ReviewsView.tsx | 200 ++++++++++++++++++ src/plugins/reviewDB/entities.ts | 100 +++++++++ src/plugins/reviewDB/index.tsx | 157 ++++++++++++++ src/plugins/reviewDB/reviewDbApi.ts | 197 +++++++++++++++++ src/plugins/reviewDB/settings.tsx | 93 ++++++++ src/plugins/reviewDB/style.css | 121 +++++++++++ src/plugins/reviewDB/utils.tsx | 43 ++++ 13 files changed, 1516 insertions(+) create mode 100644 src/plugins/reviewDB/auth.tsx create mode 100644 src/plugins/reviewDB/components/BlockedUserModal.tsx create mode 100644 src/plugins/reviewDB/components/MessageButton.tsx create mode 100644 src/plugins/reviewDB/components/ReviewBadge.tsx create mode 100644 src/plugins/reviewDB/components/ReviewComponent.tsx create mode 100644 src/plugins/reviewDB/components/ReviewModal.tsx create mode 100644 src/plugins/reviewDB/components/ReviewsView.tsx create mode 100644 src/plugins/reviewDB/entities.ts create mode 100644 src/plugins/reviewDB/index.tsx create mode 100644 src/plugins/reviewDB/reviewDbApi.ts create mode 100644 src/plugins/reviewDB/settings.tsx create mode 100644 src/plugins/reviewDB/style.css create mode 100644 src/plugins/reviewDB/utils.tsx diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx new file mode 100644 index 000000000..1d95e47dd --- /dev/null +++ b/src/plugins/reviewDB/auth.tsx @@ -0,0 +1,78 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { DataStore } from "@api/index"; +import { Logger } from "@utils/Logger"; +import { openModal } from "@utils/modal"; +import { findByPropsLazy } from "@webpack"; +import { showToast, Toasts, UserStore } from "@webpack/common"; + +import { ReviewDBAuth } from "./entities"; + +const DATA_STORE_KEY = "rdb-auth"; + +const OAuth = findByPropsLazy("OAuth2AuthorizeModal"); + +export let Auth: ReviewDBAuth = {}; + +export async function initAuth() { + Auth = await getAuth() ?? {}; +} + +export async function getAuth(): Promise { + const auth = await DataStore.get(DATA_STORE_KEY); + return auth?.[UserStore.getCurrentUser()?.id]; +} + +export async function getToken() { + const auth = await getAuth(); + return auth?.token; +} + +export async function updateAuth(newAuth: ReviewDBAuth) { + return DataStore.update(DATA_STORE_KEY, auth => { + auth ??= {}; + Auth = auth[UserStore.getCurrentUser().id] ??= {}; + + if (newAuth.token) Auth.token = newAuth.token; + if (newAuth.user) Auth.user = newAuth.user; + + return auth; + }); +} + +export function authorize(callback?: any) { + openModal(props => + { + try { + const url = new URL(response.location); + url.searchParams.append("clientMod", "vencord"); + const res = await fetch(url, { + headers: new Headers({ Accept: "application/json" }) + }); + const { token, success } = await res.json(); + if (success) { + updateAuth({ token }); + showToast("Successfully logged in!"); + callback?.(); + } else if (res.status === 1) { + showToast("An Error occurred while logging in.", Toasts.Type.FAILURE); + } + } catch (e) { + new Logger("ReviewDB").error("Failed to authorize", e); + } + }} + /> + ); +} diff --git a/src/plugins/reviewDB/components/BlockedUserModal.tsx b/src/plugins/reviewDB/components/BlockedUserModal.tsx new file mode 100644 index 000000000..43c81eb52 --- /dev/null +++ b/src/plugins/reviewDB/components/BlockedUserModal.tsx @@ -0,0 +1,99 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; +import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal"; +import { useAwaiter } from "@utils/react"; +import { Forms, Tooltip, useState } from "@webpack/common"; + +import { Auth } from "../auth"; +import { ReviewDBUser } from "../entities"; +import { fetchBlocks, unblockUser } from "../reviewDbApi"; +import { cl } from "../utils"; + +function UnblockButton(props: { onClick?(): void; }) { + return ( + + {tooltipProps => ( +
+ + + +
+ )} +
+ ); +} + +function BlockedUser({ user, isBusy, setIsBusy }: { user: ReviewDBUser; isBusy: boolean; setIsBusy(v: boolean): void; }) { + const [gone, setGone] = useState(false); + if (gone) return null; + + return ( +
+ + {user.username} + { + setIsBusy(true); + try { + await unblockUser(user.discordID); + setGone(true); + } finally { + setIsBusy(false); + } + }} + /> +
+ ); +} + +function Modal() { + const [isBusy, setIsBusy] = useState(false); + const [blocks, error, pending] = useAwaiter(fetchBlocks, { + onError: e => new Logger("ReviewDB").error("Failed to fetch blocks", e), + fallbackValue: [], + }); + + if (pending) + return null; + if (error) + return Failed to fetch blocks: ${String(error)}; + if (!blocks.length) + return No blocked users.; + + return ( + <> + {blocks.map(b => ( + + ))} + + ); +} + +export function openBlockModal() { + openModal(modalProps => ( + + + Blocked Users + + + + {Auth.token ? : You are not logged into ReviewDB!} + + + )); +} diff --git a/src/plugins/reviewDB/components/MessageButton.tsx b/src/plugins/reviewDB/components/MessageButton.tsx new file mode 100644 index 000000000..9b0b4be1a --- /dev/null +++ b/src/plugins/reviewDB/components/MessageButton.tsx @@ -0,0 +1,85 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { DeleteIcon } from "@components/Icons"; +import { classes } from "@utils/misc"; +import { findByPropsLazy } from "@webpack"; +import { Tooltip } from "@webpack/common"; + +const iconClasses = findByPropsLazy("button", "wrapper", "disabled", "separator"); + +export function DeleteButton({ onClick }: { onClick(): void; }) { + return ( + + {props => ( +
+ +
+ )} +
+ ); +} + +export function ReportButton({ onClick }: { onClick(): void; }) { + return ( + + {props => ( +
+ + + +
+ )} +
+ ); +} + +export function BlockButton({ onClick, isBlocked }: { onClick(): void; isBlocked: boolean; }) { + return ( + + {props => ( +
+ + {isBlocked + ? + : + } + +
+ )} +
+ ); +} diff --git a/src/plugins/reviewDB/components/ReviewBadge.tsx b/src/plugins/reviewDB/components/ReviewBadge.tsx new file mode 100644 index 000000000..665b9bb09 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewBadge.tsx @@ -0,0 +1,50 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { MaskedLink, React, Tooltip } from "@webpack/common"; +import { HTMLAttributes } from "react"; + +import { Badge } from "../entities"; +import { cl } from "../utils"; + +export default function ReviewBadge(badge: Badge & { onClick?(): void; }) { + const Wrapper = badge.redirectURL + ? MaskedLink + : (props: HTMLAttributes) => ( + {props.children} + ); + + return ( + + {({ onMouseEnter, onMouseLeave }) => ( + + {badge.description} + + )} + + ); +} diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx new file mode 100644 index 000000000..5f3d135b4 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewComponent.tsx @@ -0,0 +1,188 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { openUserProfile } from "@utils/discord"; +import { classes } from "@utils/misc"; +import { LazyComponent } from "@utils/react"; +import { filters, findBulk } from "@webpack"; +import { Alerts, moment, Parser, showToast, Timestamp } from "@webpack/common"; + +import { Auth, getToken } from "../auth"; +import { Review, ReviewType } from "../entities"; +import { blockUser, deleteReview, reportReview, unblockUser } from "../reviewDbApi"; +import { settings } from "../settings"; +import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl } from "../utils"; +import { openBlockModal } from "./BlockedUserModal"; +import { BlockButton, DeleteButton, ReportButton } from "./MessageButton"; +import ReviewBadge from "./ReviewBadge"; + +export default LazyComponent(() => { + // this is terrible, blame ven + const p = filters.byProps; + const [ + { cozyMessage, buttons, message, buttonsInner, groupStart }, + { container, isHeader }, + { avatar, clickable, username, wrapper, cozy }, + buttonClasses, + botTag + ] = findBulk( + p("cozyMessage"), + p("container", "isHeader"), + p("avatar", "zalgo"), + p("button", "wrapper", "selected"), + p("botTag", "botTagRegular") + ); + + const dateFormat = new Intl.DateTimeFormat(); + + return function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) { + function openModal() { + openUserProfile(review.sender.discordID); + } + + function delReview() { + Alerts.show({ + title: "Are you sure?", + body: "Do you really want to delete this review?", + confirmText: "Delete", + cancelText: "Nevermind", + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to delete reviews."); + } else { + deleteReview(review.id).then(res => { + if (res.success) { + refetch(); + } + showToast(res.message); + }); + } + } + }); + } + + function reportRev() { + Alerts.show({ + title: "Are you sure?", + body: "Do you really you want to report this review?", + confirmText: "Report", + cancelText: "Nevermind", + // confirmColor: "red", this just adds a class name and breaks the submit button guh + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to report reviews."); + } else { + reportReview(review.id); + } + } + }); + } + + const isAuthorBlocked = Auth?.user?.blockedUsers?.includes(review.sender.discordID) ?? false; + + function blockReviewSender() { + if (isAuthorBlocked) + return unblockUser(review.sender.discordID); + + Alerts.show({ + title: "Are you sure?", + body: "Do you really you want to block this user? They will be unable to leave further reviews on your profile. You can unblock users in the plugin settings.", + confirmText: "Block", + cancelText: "Nevermind", + // confirmColor: "red", this just adds a class name and breaks the submit button guh + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to block users."); + } else { + blockUser(review.sender.discordID); + } + } + }); + } + + return ( +
+ + +
+ openModal()} + > + {review.sender.username} + + + {review.type === ReviewType.System && ( + + + System + + + )} +
+ {isAuthorBlocked && ( + openBlockModal()} + /> + )} + {review.sender.badges.map(badge => )} + + { + !settings.store.hideTimestamps && review.type !== ReviewType.System && ( + + {dateFormat.format(review.timestamp * 1000)} + ) + } + +
+ {Parser.parseGuildEventDescription(review.comment)} +
+ + {review.id !== 0 && ( +
+
+ {canReportReview(review) && } + {canBlockReviewAuthor(profileId, review) && } + {canDeleteReview(profileId, review) && } +
+
+ )} +
+ ); + }; +}); diff --git a/src/plugins/reviewDB/components/ReviewModal.tsx b/src/plugins/reviewDB/components/ReviewModal.tsx new file mode 100644 index 000000000..9669a2b32 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewModal.tsx @@ -0,0 +1,105 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { useForceUpdater } from "@utils/react"; +import { Paginator, Text, useRef, useState } from "@webpack/common"; + +import { Auth } from "../auth"; +import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; +import { cl } from "../utils"; +import ReviewComponent from "./ReviewComponent"; +import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; + +function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: string; name: string; }) { + const [data, setData] = useState(); + const [signal, refetch] = useForceUpdater(true); + const [page, setPage] = useState(1); + + const ref = useRef(null); + + const reviewCount = data?.reviewCount; + const ownReview = data?.reviews.find(r => r.sender.discordID === Auth.user?.discordID); + + return ( + + + + + {name}'s Reviews + {!!reviewCount && ({reviewCount} Reviews)} + + + + + +
+ ref.current?.scrollTo({ top: 0, behavior: "smooth" })} + hideOwnReview + /> +
+
+ + +
+ {ownReview && ( + + )} + + + {!!reviewCount && ( + + )} +
+
+
+
+ ); +} + +export function openReviewsModal(discordId: string, name: string) { + openModal(props => ( + + )); +} diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx new file mode 100644 index 000000000..abb856b99 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -0,0 +1,200 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react"; +import { find, findByPropsLazy } from "@webpack"; +import { Forms, React, RelationshipStore, showToast, useRef, UserStore } from "@webpack/common"; + +import { Auth, authorize } from "../auth"; +import { Review } from "../entities"; +import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; +import { settings } from "../settings"; +import { cl } from "../utils"; +import ReviewComponent from "./ReviewComponent"; + + +const Slate = findByPropsLazy("Editor", "Transforms"); +const InputTypes = findByPropsLazy("ChatInputTypes"); + +const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); + +interface UserProps { + discordId: string; + name: string; +} + +interface Props extends UserProps { + onFetchReviews(data: Response): void; + refetchSignal?: unknown; + showInput?: boolean; + page?: number; + scrollToTop?(): void; + hideOwnReview?: boolean; +} + +export default function ReviewsView({ + discordId, + name, + onFetchReviews, + refetchSignal, + scrollToTop, + page = 1, + showInput = false, + hideOwnReview = false, +}: Props) { + const [signal, refetch] = useForceUpdater(true); + + const [reviewData] = useAwaiter(() => getReviews(discordId, (page - 1) * REVIEWS_PER_PAGE), { + fallbackValue: null, + deps: [refetchSignal, signal, page], + onSuccess: data => { + if (settings.store.hideBlockedUsers) + data!.reviews = data!.reviews?.filter(r => !RelationshipStore.isBlocked(r.sender.discordID)); + + scrollToTop?.(); + onFetchReviews(data!); + } + }); + + if (!reviewData) return null; + + return ( + <> + + + {showInput && ( + r.sender.discordID === UserStore.getCurrentUser().id)} + /> + )} + + ); +} + +function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; }) { + const myId = UserStore.getCurrentUser().id; + + return ( +
+ {reviews?.map(review => + (review.sender.discordID !== myId || !hideOwnReview) && + + )} + + {reviews?.length === 0 && ( + + Looks like nobody reviewed this user yet. You could be the first! + + )} +
+ ); +} + + +export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) { + const { token } = Auth; + const editorRef = useRef(null); + const inputType = InputTypes.ChatInputTypes.FORM; + inputType.disableAutoFocus = true; + + const channel = { + flags_: 256, + guild_id_: null, + id: "0", + getGuildId: () => null, + isPrivate: () => true, + isActiveThread: () => false, + isArchivedLockedThread: () => false, + isDM: () => true, + roles: { "0": { permissions: 0n } }, + getRecipientId: () => "0", + hasFlag: () => false, + }; + + return ( + <> +
{ + if (!token) { + showToast("Opening authorization window..."); + authorize(); + } + }}> + editorRef.current = ref} + textValue="" + onSubmit={ + async res => { + const response = await addReview({ + userid: discordId, + comment: res.value, + }); + + if (response?.success) { + refetch(); + + const slateEditor = editorRef.current.ref.current.getSlateEditor(); + const { Editor, Transform } = Slate; + + // clear editor + Transform.delete(slateEditor, { + at: { + anchor: Editor.start(slateEditor, []), + focus: Editor.end(slateEditor, []), + } + }); + } else if (response?.message) { + showToast(response.message); + } + + // even tho we need to return this, it doesnt do anything + return { + shouldClear: false, + shouldRefocus: true, + }; + } + } + /> +
+ + + ); +} diff --git a/src/plugins/reviewDB/entities.ts b/src/plugins/reviewDB/entities.ts new file mode 100644 index 000000000..a871d90f2 --- /dev/null +++ b/src/plugins/reviewDB/entities.ts @@ -0,0 +1,100 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +export const enum UserType { + Banned = -1, + Normal = 0, + Admin = 1 +} + +export const enum ReviewType { + User = 0, + Server = 1, + Support = 2, + System = 3 +} + +export const enum NotificationType { + Info = 0, + Ban = 1, + Unban = 2, + Warning = 3 +} + +export interface ReviewDBAuth { + token?: string; + user?: ReviewDBCurrentUser; +} + +export interface Badge { + name: string; + description: string; + icon: string; + redirectURL?: string; + type: number; +} + +export interface BanInfo { + id: string; + discordID: string; + reviewID: number; + reviewContent: string; + banEndDate: number; +} + +export interface Notification { + id: number; + title: string; + content: string; + type: NotificationType; +} + +export interface ReviewDBUser { + ID: number; + discordID: string; + username: string; + type: UserType; + profilePhoto: string; + badges: any[]; +} + +export interface ReviewDBCurrentUser extends ReviewDBUser { + warningCount: number; + clientMod: string; + banInfo: BanInfo | null; + notification: Notification | null; + lastReviewID: number; + blockedUsers?: string[]; +} + +export interface ReviewAuthor { + id: number, + discordID: string, + username: string, + profilePhoto: string, + badges: Badge[]; +} + +export interface Review { + comment: string, + id: number, + star: number, + sender: ReviewAuthor, + timestamp: number; + type?: ReviewType; +} diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx new file mode 100644 index 000000000..d8357faf2 --- /dev/null +++ b/src/plugins/reviewDB/index.tsx @@ -0,0 +1,157 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import "./style.css"; + +import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import ErrorBoundary from "@components/ErrorBoundary"; +import ExpandableHeader from "@components/ExpandableHeader"; +import { OpenExternalIcon } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin from "@utils/types"; +import { Alerts, Menu, Parser, showToast, useState } from "@webpack/common"; +import { Guild, User } from "discord-types/general"; + +import { Auth, initAuth, updateAuth } from "./auth"; +import { openReviewsModal } from "./components/ReviewModal"; +import ReviewsView from "./components/ReviewsView"; +import { NotificationType } from "./entities"; +import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; +import { settings } from "./settings"; + +const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => { + children.push( + openReviewsModal(props.guild.id, props.guild.name)} + /> + ); +}; + +export default definePlugin({ + name: "ReviewDB", + description: "Review other users (Adds a new settings to profiles)", + authors: [Devs.mantikafasi, Devs.Ven], + + settings, + + patches: [ + { + find: "showBorder:null", + replacement: { + match: /user:(\i),setNote:\i,canDM.+?\}\)/, + replace: "$&,$self.getReviewsComponent($1)" + } + } + ], + + flux: { + CONNECTION_OPEN: initAuth, + }, + + async start() { + addContextMenuPatch("guild-header-popout", guildPopoutPatch); + + const s = settings.store; + const { lastReviewId, notifyReviews } = s; + + const legacy = s as any as { token?: string; }; + if (legacy.token) { + await updateAuth({ token: legacy.token }); + legacy.token = undefined; + new Logger("ReviewDB").info("Migrated legacy settings"); + } + + await initAuth(); + + setTimeout(async () => { + if (!Auth.token) return; + + const user = await getCurrentUserInfo(Auth.token); + updateAuth({ user }); + + if (notifyReviews) { + if (lastReviewId && lastReviewId < user.lastReviewID) { + s.lastReviewId = user.lastReviewID; + if (user.lastReviewID !== 0) + showToast("You have new reviews on your profile!"); + } + } + + if (user.notification) { + const props = user.notification.type === NotificationType.Ban ? { + cancelText: "Appeal", + confirmText: "Ok", + onCancel: async () => + VencordNative.native.openExternal( + "https://reviewdb.mantikafasi.dev/api/redirect?" + + new URLSearchParams({ + token: Auth.token!, + page: "dashboard/appeal" + }) + ) + } : {}; + + Alerts.show({ + title: user.notification.title, + body: ( + Parser.parse( + user.notification.content, + false + ) + ), + ...props + }); + + readNotification(user.notification.id); + } + }, 4000); + }, + + stop() { + removeContextMenuPatch("guild-header-popout", guildPopoutPatch); + }, + + getReviewsComponent: ErrorBoundary.wrap((user: User) => { + const [reviewCount, setReviewCount] = useState(); + + return ( + openReviewsModal(user.id, user.username)} + moreTooltipText={ + reviewCount && reviewCount > 50 + ? `View all ${reviewCount} reviews` + : "Open Review Modal" + } + onDropDownClick={state => settings.store.reviewsDropdownState = !state} + defaultState={settings.store.reviewsDropdownState} + > + setReviewCount(r.reviewCount)} + showInput + /> + + ); + }, { message: "Failed to render Reviews" }) +}); diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts new file mode 100644 index 000000000..a87fbcb8f --- /dev/null +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -0,0 +1,197 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { showToast, Toasts } from "@webpack/common"; + +import { Auth, authorize, getToken, updateAuth } from "./auth"; +import { Review, ReviewDBCurrentUser, ReviewDBUser } from "./entities"; +import { settings } from "./settings"; + +const API_URL = "https://manti.vendicated.dev"; + +export const REVIEWS_PER_PAGE = 50; + +export interface Response { + success: boolean, + message: string; + reviews: Review[]; + updated: boolean; + hasNextPage: boolean; + reviewCount: number; +} + +const WarningFlag = 0b00000010; + +export async function getReviews(id: string, offset = 0): Promise { + let flags = 0; + if (!settings.store.showWarning) flags |= WarningFlag; + + const params = new URLSearchParams({ + flags: String(flags), + offset: String(offset) + }); + const req = await fetch(`${API_URL}/api/reviewdb/users/${id}/reviews?${params}`); + + const res = (req.status === 200) + ? await req.json() as Response + : { + success: false, + message: "An Error occured while fetching reviews. Please try again later.", + reviews: [], + updated: false, + hasNextPage: false, + reviewCount: 0 + }; + + if (!res.success) { + showToast(res.message, Toasts.Type.FAILURE); + return { + ...res, + reviews: [ + { + id: 0, + comment: "An Error occured while fetching reviews. Please try again later.", + star: 0, + timestamp: 0, + sender: { + id: 0, + username: "Error", + profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128", + discordID: "0", + badges: [] + } + } + ] + }; + } + + return res; +} + +export async function addReview(review: any): Promise { + review.token = await getToken(); + + if (!review.token) { + showToast("Please authorize to add a review."); + authorize(); + return null; + } + + return fetch(API_URL + `/api/reviewdb/users/${review.userid}/reviews`, { + method: "PUT", + body: JSON.stringify(review), + headers: { + "Content-Type": "application/json", + } + }) + .then(r => r.json()) + .then(res => { + showToast(res.message); + return res ?? null; + }); +} + +export async function deleteReview(id: number): Promise { + return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, { + method: "DELETE", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + }), + body: JSON.stringify({ + token: await getToken(), + reviewid: id + }) + }).then(r => r.json()); +} + +export async function reportReview(id: number) { + const res = await fetch(API_URL + "/api/reviewdb/reports", { + method: "PUT", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + }), + body: JSON.stringify({ + reviewid: id, + token: await getToken() + }) + }).then(r => r.json()) as Response; + + showToast(res.message); +} + +async function patchBlock(action: "block" | "unblock", userId: string) { + const res = await fetch(API_URL + "/api/reviewdb/blocks", { + method: "PATCH", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + Authorization: await getToken() || "" + }), + body: JSON.stringify({ + action: action, + discordId: userId + }) + }); + + if (!res.ok) { + showToast(`Failed to ${action} user`, Toasts.Type.FAILURE); + } else { + showToast(`Successfully ${action}ed user`, Toasts.Type.SUCCESS); + + if (Auth?.user?.blockedUsers) { + const newBlockedUsers = action === "block" + ? [...Auth.user.blockedUsers, userId] + : Auth.user.blockedUsers.filter(id => id !== userId); + updateAuth({ user: { ...Auth.user, blockedUsers: newBlockedUsers } }); + } + } +} + +export const blockUser = (userId: string) => patchBlock("block", userId); +export const unblockUser = (userId: string) => patchBlock("unblock", userId); + +export async function fetchBlocks(): Promise { + const res = await fetch(API_URL + "/api/reviewdb/blocks", { + method: "GET", + headers: new Headers({ + Accept: "application/json", + Authorization: await getToken() || "" + }) + }); + + if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`); + return res.json(); +} + +export function getCurrentUserInfo(token: string): Promise { + return fetch(API_URL + "/api/reviewdb/users", { + body: JSON.stringify({ token }), + method: "POST", + }).then(r => r.json()); +} + +export async function readNotification(id: number) { + return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, { + method: "PATCH", + headers: { + "Authorization": await getToken() || "", + }, + }); +} diff --git a/src/plugins/reviewDB/settings.tsx b/src/plugins/reviewDB/settings.tsx new file mode 100644 index 000000000..efcb80588 --- /dev/null +++ b/src/plugins/reviewDB/settings.tsx @@ -0,0 +1,93 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; +import { Button } from "@webpack/common"; + +import { authorize, getToken } from "./auth"; +import { openBlockModal } from "./components/BlockedUserModal"; + +export const settings = definePluginSettings({ + authorize: { + type: OptionType.COMPONENT, + description: "Authorize with ReviewDB", + component: () => ( + + ) + }, + notifyReviews: { + type: OptionType.BOOLEAN, + description: "Notify about new reviews on startup", + default: true, + }, + showWarning: { + type: OptionType.BOOLEAN, + description: "Display warning to be respectful at the top of the reviews list", + default: true, + }, + hideTimestamps: { + type: OptionType.BOOLEAN, + description: "Hide timestamps on reviews", + default: false, + }, + hideBlockedUsers: { + type: OptionType.BOOLEAN, + description: "Hide reviews from blocked users", + default: true, + }, + manageBlocks: { + type: OptionType.COMPONENT, + description: "Manage Blocked Users", + component: () => ( + + ) + }, + website: { + type: OptionType.COMPONENT, + description: "ReviewDB website", + component: () => ( + + ) + }, + supportServer: { + type: OptionType.COMPONENT, + description: "ReviewDB Support Server", + component: () => ( + + ) + } +}).withPrivateSettings<{ + lastReviewId?: number; + reviewsDropdownState?: boolean; +}>(); diff --git a/src/plugins/reviewDB/style.css b/src/plugins/reviewDB/style.css new file mode 100644 index 000000000..a812ecaf2 --- /dev/null +++ b/src/plugins/reviewDB/style.css @@ -0,0 +1,121 @@ +[class|="section"]:not([class|="lastSection"]) + .vc-rdb-view { + margin-top: 12px; +} + +.vc-rdb-badge { + vertical-align: middle; + margin-left: 4px; +} + +.vc-rdb-input { + margin-top: 6px; + margin-bottom: 12px; + resize: none; + overflow: hidden; + background: transparent; + border: 1px solid var(--profile-message-input-border-color); +} + +.vc-rdb-modal-footer > div { + width: 100%; + margin: 6px 16px; +} + +/* When input becomes disabled(while sending review), input adds unneccesary padding to left, this prevents it */ +.vc-rdb-input > div > div { + padding-left: 0 !important; +} + +.vc-rdb-placeholder { + margin-bottom: 4px; + font-weight: bold; + font-style: italic; + color: var(--text-muted); +} + +.vc-rdb-input * { + font-size: 14px; +} + +.vc-rdb-modal-footer { + padding: 0; +} + +.vc-rdb-modal-footer .vc-rdb-input { + margin-bottom: 0; + background: var(--input-background); +} + +.vc-rdb-modal-footer [class|="pageControlContainer"] { + margin-top: 0; +} + +.vc-rdb-modal-header { + flex-grow: 1; +} + +.vc-rdb-modal-reviews { + margin-top: 16px; +} + +.vc-rdb-review { + margin-top: 8px; + margin-bottom: 8px; +} + +.vc-rdb-review-comment img { + vertical-align: text-top; +} + +.vc-rdb-review-comment { + overflow-y: hidden; + margin-top: 1px; + margin-bottom: 8px; + color: var(--text-normal); + font-size: 15px; +} + +.vc-rdb-blocked-badge { + cursor: pointer; +} + +.vc-rdb-block-modal-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.vc-rdb-block-modal { + padding: 1em; + display: grid; + gap: 0.75em; +} + +.vc-rdb-block-modal-row { + display: flex; + height: 2em; + gap: 0.5em; + align-items: center; +} + +.vc-rdb-block-modal-row img { + border-radius: 50%; + height: 2em; + width: 2em; +} + +.vc-rdb-block-modal img::before { + content: ""; + display: block; + width: 100%; + height: 100%; + background-color: var(--background-modifier-accent); +} + +.vc-rdb-block-modal-username { + flex-grow: 1; +} + +.vc-rdb-block-modal-unblock { + cursor: pointer; +} diff --git a/src/plugins/reviewDB/utils.tsx b/src/plugins/reviewDB/utils.tsx new file mode 100644 index 000000000..eeaca1204 --- /dev/null +++ b/src/plugins/reviewDB/utils.tsx @@ -0,0 +1,43 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { classNameFactory } from "@api/Styles"; +import { UserStore } from "@webpack/common"; + +import { Auth } from "./auth"; +import { Review, UserType } from "./entities"; + +export const cl = classNameFactory("vc-rdb-"); + +export function canDeleteReview(profileId: string, review: Review) { + const myId = UserStore.getCurrentUser().id; + return ( + myId === profileId + || review.sender.discordID === myId + || Auth.user?.type === UserType.Admin + ); +} + +export function canBlockReviewAuthor(profileId: string, review: Review) { + const myId = UserStore.getCurrentUser().id; + return profileId === myId && review.sender.discordID !== myId; +} + +export function canReportReview(review: Review) { + return review.sender.discordID !== UserStore.getCurrentUser().id; +} From a171b35e97c8931e2466a414a65463f349fb7b11 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Mon, 15 Jan 2024 17:36:24 +0100 Subject: [PATCH 24/50] ReviewDB fixes --- src/plugins/reviewDB/auth.tsx | 6 ++-- .../reviewDB/components/ReviewComponent.tsx | 2 +- .../reviewDB/components/ReviewsView.tsx | 9 +++--- src/plugins/reviewDB/reviewDbApi.ts | 31 ++++++++++--------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx index 1d95e47dd..e7a369217 100644 --- a/src/plugins/reviewDB/auth.tsx +++ b/src/plugins/reviewDB/auth.tsx @@ -14,7 +14,7 @@ import { ReviewDBAuth } from "./entities"; const DATA_STORE_KEY = "rdb-auth"; -const OAuth = findByPropsLazy("OAuth2AuthorizeModal"); +const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal"); export let Auth: ReviewDBAuth = {}; @@ -46,7 +46,7 @@ export async function updateAuth(newAuth: ReviewDBAuth) { export function authorize(callback?: any) { openModal(props => - { - // this is terrible, blame ven + // this is terrible, blame mantika const p = filters.byProps; const [ { cozyMessage, buttons, message, buttonsInner, groupStart }, diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index abb856b99..cfd5477db 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -28,8 +28,8 @@ import { cl } from "../utils"; import ReviewComponent from "./ReviewComponent"; -const Slate = findByPropsLazy("Editor", "Transforms"); -const InputTypes = findByPropsLazy("ChatInputTypes"); +const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms"); +const { ChatInputTypes } = findByPropsLazy("ChatInputTypes"); const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); @@ -122,7 +122,7 @@ function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch(): export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) { const { token } = Auth; const editorRef = useRef(null); - const inputType = InputTypes.ChatInputTypes.FORM; + const inputType = ChatInputTypes.FORM; inputType.disableAutoFocus = true; const channel = { @@ -172,10 +172,9 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { refetch(); const slateEditor = editorRef.current.ref.current.getSlateEditor(); - const { Editor, Transform } = Slate; // clear editor - Transform.delete(slateEditor, { + Transforms.delete(slateEditor, { at: { anchor: Editor.start(slateEditor, []), focus: Editor.end(slateEditor, []), diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts index a87fbcb8f..657e9c475 100644 --- a/src/plugins/reviewDB/reviewDbApi.ts +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -19,10 +19,10 @@ import { showToast, Toasts } from "@webpack/common"; import { Auth, authorize, getToken, updateAuth } from "./auth"; -import { Review, ReviewDBCurrentUser, ReviewDBUser } from "./entities"; +import { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from "./entities"; import { settings } from "./settings"; -const API_URL = "https://manti.vendicated.dev"; +const API_URL = "https://manti.vendicated.dev/api/reviewdb"; export const REVIEWS_PER_PAGE = 50; @@ -45,13 +45,13 @@ export async function getReviews(id: string, offset = 0): Promise { flags: String(flags), offset: String(offset) }); - const req = await fetch(`${API_URL}/api/reviewdb/users/${id}/reviews?${params}`); + const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`); const res = (req.status === 200) ? await req.json() as Response : { success: false, - message: "An Error occured while fetching reviews. Please try again later.", + message: req.status === 429 ? "You are sending requests too fast. Wait a few seconds and try again." : "An Error occured while fetching reviews. Please try again later.", reviews: [], updated: false, hasNextPage: false, @@ -65,14 +65,15 @@ export async function getReviews(id: string, offset = 0): Promise { reviews: [ { id: 0, - comment: "An Error occured while fetching reviews. Please try again later.", + comment: res.message, star: 0, timestamp: 0, + type: ReviewType.System, sender: { id: 0, - username: "Error", - profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128", - discordID: "0", + username: "ReviewDB", + profilePhoto: "https://cdn.discordapp.com/avatars/1134864775000629298/3f87ad315b32ee464d84f1270c8d1b37.png?size=256&format=webp&quality=lossless", + discordID: "1134864775000629298", badges: [] } } @@ -92,7 +93,7 @@ export async function addReview(review: any): Promise { return null; } - return fetch(API_URL + `/api/reviewdb/users/${review.userid}/reviews`, { + return fetch(API_URL + `/users/${review.userid}/reviews`, { method: "PUT", body: JSON.stringify(review), headers: { @@ -107,7 +108,7 @@ export async function addReview(review: any): Promise { } export async function deleteReview(id: number): Promise { - return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, { + return fetch(API_URL + `/users/${id}/reviews`, { method: "DELETE", headers: new Headers({ "Content-Type": "application/json", @@ -121,7 +122,7 @@ export async function deleteReview(id: number): Promise { } export async function reportReview(id: number) { - const res = await fetch(API_URL + "/api/reviewdb/reports", { + const res = await fetch(API_URL + "/reports", { method: "PUT", headers: new Headers({ "Content-Type": "application/json", @@ -137,7 +138,7 @@ export async function reportReview(id: number) { } async function patchBlock(action: "block" | "unblock", userId: string) { - const res = await fetch(API_URL + "/api/reviewdb/blocks", { + const res = await fetch(API_URL + "/blocks", { method: "PATCH", headers: new Headers({ "Content-Type": "application/json", @@ -168,7 +169,7 @@ export const blockUser = (userId: string) => patchBlock("block", userId); export const unblockUser = (userId: string) => patchBlock("unblock", userId); export async function fetchBlocks(): Promise { - const res = await fetch(API_URL + "/api/reviewdb/blocks", { + const res = await fetch(API_URL + "/blocks", { method: "GET", headers: new Headers({ Accept: "application/json", @@ -181,14 +182,14 @@ export async function fetchBlocks(): Promise { } export function getCurrentUserInfo(token: string): Promise { - return fetch(API_URL + "/api/reviewdb/users", { + return fetch(API_URL + "/users", { body: JSON.stringify({ token }), method: "POST", }).then(r => r.json()); } export async function readNotification(id: number) { - return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, { + return fetch(API_URL + `/notifications?id=${id}`, { method: "PATCH", headers: { "Authorization": await getToken() || "", From 1a982ae9aa51081f12c34c5dd68857bfbb4f312e Mon Sep 17 00:00:00 2001 From: rini Date: Mon, 15 Jan 2024 23:51:12 -0300 Subject: [PATCH 25/50] feat(anonymisefilenames): add icon to toggle anonymising (#2087) Co-authored-by: V Co-authored-by: fawn --- src/plugins/anonymiseFileNames/index.ts | 94 ---------------- src/plugins/anonymiseFileNames/index.tsx | 130 +++++++++++++++++++++++ 2 files changed, 130 insertions(+), 94 deletions(-) delete mode 100644 src/plugins/anonymiseFileNames/index.ts create mode 100644 src/plugins/anonymiseFileNames/index.tsx diff --git a/src/plugins/anonymiseFileNames/index.ts b/src/plugins/anonymiseFileNames/index.ts deleted file mode 100644 index 9e69d7a93..000000000 --- a/src/plugins/anonymiseFileNames/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Settings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const enum Methods { - Random, - Consistent, - Timestamp, -} - -const tarExtMatcher = /\.tar\.\w+$/; - -export default definePlugin({ - name: "AnonymiseFileNames", - authors: [Devs.obscurity], - description: "Anonymise uploaded file names", - patches: [ - { - find: "instantBatchUpload:function", - replacement: { - match: /uploadFiles:(.{1,2}),/, - replace: - "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),", - }, - }, - ], - - options: { - method: { - description: "Anonymising method", - type: OptionType.SELECT, - options: [ - { label: "Random Characters", value: Methods.Random, default: true }, - { label: "Consistent", value: Methods.Consistent }, - { label: "Timestamp (4chan-like)", value: Methods.Timestamp }, - ], - }, - randomisedLength: { - description: "Random characters length", - type: OptionType.NUMBER, - default: 7, - disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Random, - }, - consistent: { - description: "Consistent filename", - type: OptionType.STRING, - default: "image", - disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Consistent, - }, - }, - - anonymise(file: string) { - let name = "image"; - const tarMatch = tarExtMatcher.exec(file); - const extIdx = tarMatch?.index ?? file.lastIndexOf("."); - const ext = extIdx !== -1 ? file.slice(extIdx) : ""; - - switch (Settings.plugins.AnonymiseFileNames.method) { - case Methods.Random: - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - name = Array.from( - { length: Settings.plugins.AnonymiseFileNames.randomisedLength }, - () => chars[Math.floor(Math.random() * chars.length)] - ).join(""); - break; - case Methods.Consistent: - name = Settings.plugins.AnonymiseFileNames.consistent; - break; - case Methods.Timestamp: - // UNIX timestamp in nanos, i could not find a better dependency-less way - name = `${Math.floor(Date.now() / 1000)}${Math.floor(window.performance.now())}`; - break; - } - return name + ext; - }, -}); diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx new file mode 100644 index 000000000..845aa756d --- /dev/null +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -0,0 +1,130 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Upload } from "@api/MessageEvents"; +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; + +type AnonUpload = Upload & { anonymise?: boolean; }; + +const ActionBarIcon = findByCodeLazy(".actionBarIcon)"); +const UploadDraft = findByPropsLazy("popFirstFile", "update"); + +const enum Methods { + Random, + Consistent, + Timestamp, +} + +const tarExtMatcher = /\.tar\.\w+$/; + +const settings = definePluginSettings({ + anonymiseByDefault: { + description: "Whether to anonymise file names by default", + type: OptionType.BOOLEAN, + default: true, + }, + method: { + description: "Anonymising method", + type: OptionType.SELECT, + options: [ + { label: "Random Characters", value: Methods.Random, default: true }, + { label: "Consistent", value: Methods.Consistent }, + { label: "Timestamp", value: Methods.Timestamp }, + ], + }, + randomisedLength: { + description: "Random characters length", + type: OptionType.NUMBER, + default: 7, + disabled: () => settings.store.method !== Methods.Random, + }, + consistent: { + description: "Consistent filename", + type: OptionType.STRING, + default: "image", + disabled: () => settings.store.method !== Methods.Consistent, + }, +}); + +export default definePlugin({ + name: "AnonymiseFileNames", + authors: [Devs.obscurity], + description: "Anonymise uploaded file names", + patches: [ + { + find: "instantBatchUpload:function", + replacement: { + match: /uploadFiles:(.{1,2}),/, + replace: + "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),", + }, + }, + { + find: ".Messages.ATTACHMENT_UTILITIES_SPOILER", + replacement: { + match: /(?<=children:\[)(?=.{10,80}tooltip:\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/, + replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null," + }, + }, + ], + settings, + + renderIcon: ErrorBoundary.wrap(({ upload, channelId, draftType }: { upload: AnonUpload; draftType: unknown; channelId: string; }) => { + const anonymise = upload.anonymise ?? settings.store.anonymiseByDefault; + return ( + { + upload.anonymise = !anonymise; + UploadDraft.update(channelId, upload.id, draftType, {}); // dummy update so component rerenders + }} + > + {anonymise + ? + : + } + + ); + }, { noop: true }), + + anonymise(upload: AnonUpload) { + if ((upload.anonymise ?? settings.store.anonymiseByDefault) === false) return upload.filename; + + const file = upload.filename; + const tarMatch = tarExtMatcher.exec(file); + const extIdx = tarMatch?.index ?? file.lastIndexOf("."); + const ext = extIdx !== -1 ? file.slice(extIdx) : ""; + + switch (settings.store.method) { + case Methods.Random: + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + return Array.from( + { length: settings.store.randomisedLength }, + () => chars[Math.floor(Math.random() * chars.length)] + ).join("") + ext; + case Methods.Consistent: + return settings.store.consistent + ext; + case Methods.Timestamp: + return Date.now() + ext; + } + }, +}); From f14001b531d4adc6cc97d382c481b14de274b234 Mon Sep 17 00:00:00 2001 From: CodeF53 <37855219+CodeF53@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:53:27 -0700 Subject: [PATCH 26/50] ClientTheme light mode support, shortcut buttons (#2010) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Co-authored-by: V --- src/components/VencordSettings/ThemesTab.tsx | 17 ++ src/plugins/clientTheme/clientTheme.css | 12 +- src/plugins/clientTheme/index.tsx | 209 +++++++++++++------ 3 files changed, 173 insertions(+), 65 deletions(-) diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 2808494a1..8abaaba4f 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -21,9 +21,11 @@ import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; import { DeleteIcon } from "@components/Icons"; import { Link } from "@components/Link"; +import PluginModal from "@components/PluginSettings/PluginModal"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; +import { openModal } from "@utils/modal"; import { showItemInFolder } from "@utils/native"; import { useAwaiter } from "@utils/react"; import { findByPropsLazy, findLazy } from "@webpack"; @@ -248,6 +250,21 @@ function ThemesTab() { > Edit QuickCSS + + {Vencord.Settings.plugins.ClientTheme.enabled && ( + + )} diff --git a/src/plugins/clientTheme/clientTheme.css b/src/plugins/clientTheme/clientTheme.css index 023f88bd2..64aefdcf5 100644 --- a/src/plugins/clientTheme/clientTheme.css +++ b/src/plugins/clientTheme/clientTheme.css @@ -19,6 +19,16 @@ border: thin solid var(--background-modifier-accent) !important; } -.client-theme-warning { +.client-theme-warning * { color: var(--text-danger); } + +.client-theme-contrast-warning { + background-color: var(--background-primary); + padding: 0.5rem; + border-radius: .5rem; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index d75929961..5d8cd4dc0 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -8,19 +8,19 @@ import "./clientTheme.css"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import { getTheme, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import definePlugin, { OptionType, StartAt } from "@utils/types"; -import { findComponentByCodeLazy } from "@webpack"; -import { Button, Forms } from "@webpack/common"; +import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; +import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common"; const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const colorPresets = [ "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D", "#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42", - "#3C2E42", "#422938" + "#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d", + "#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb", ]; function onPickColor(color: number) { @@ -30,9 +30,35 @@ function onPickColor(color: number) { updateColorVars(hexColor); } +const { saveClientTheme } = findByPropsLazy("saveClientTheme"); + +function setTheme(theme: string) { + saveClientTheme({ theme }); +} + +const ThemeStore = findStoreLazy("ThemeStore"); +const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore"); + function ThemeSettings() { - const lightnessWarning = hexToLightness(settings.store.color) > 45; - const lightModeWarning = getTheme() === Theme.Light; + const theme = useStateFromStores([ThemeStore], () => ThemeStore.theme); + const isLightTheme = theme === "light"; + const oppositeTheme = isLightTheme ? "dark" : "light"; + + const nitroTheme = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset); + const nitroThemeEnabled = nitroTheme !== undefined; + + const selectedLuminance = relativeLuminance(settings.store.color); + + let contrastWarning = false, fixableContrast = true; + if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12) + contrastWarning = true; + if (selectedLuminance < 0.26 && selectedLuminance > 0.12) + fixableContrast = false; + // light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels + if (isLightTheme && selectedLuminance > 0.65) { + contrastWarning = true; + fixableContrast = false; + } return (
@@ -48,15 +74,18 @@ function ThemeSettings() { suggestedColors={colorPresets} />
- {lightnessWarning || lightModeWarning - ?
- - Your theme won't look good: - {lightnessWarning && Selected color is very light} - {lightModeWarning && Light mode isn't supported} + {(contrastWarning || nitroThemeEnabled) && (<> + +
+
+ Warning, your theme won't look good: + {contrastWarning && Selected color won't contrast well with text} + {nitroThemeEnabled && Nitro themes aren't supported} +
+ {(contrastWarning && fixableContrast) && } + {(nitroThemeEnabled) && }
- : null - } + )}
); } @@ -87,9 +116,12 @@ export default definePlugin({ settings, startAt: StartAt.DOMContentLoaded, - start() { + async start() { updateColorVars(settings.store.color); - generateColorOffsets(); + + const styles = await getStyles(); + generateColorOffsets(styles); + generateLightModeFixes(styles); }, stop() { @@ -98,56 +130,86 @@ export default definePlugin({ } }); -const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g; +const variableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g; +const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g; +const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g; -async function generateColorOffsets() { - - const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); - const variableLightness = {} as Record; - - // Search all stylesheets for color variables - for (const styleLinkNode of styleLinkNodes) { - const cssLink = styleLinkNode.getAttribute("href"); - if (!cssLink) continue; - - const res = await fetch(cssLink); - const cssString = await res.text(); - - // Get lightness values of --primary variables >=500 - let variableMatch = variableRegex.exec(cssString); - while (variableMatch !== null) { - const [, variable, lightness] = variableMatch; - variableLightness[variable] = parseFloat(lightness); - variableMatch = variableRegex.exec(cssString); - } - } - - // Generate offsets - const lightnessOffsets = Object.entries(variableLightness) +// generates variables per theme by: +// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable) +// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600) +function genThemeSpecificOffsets(variableLightness: Record, regex: RegExp, centerVariable: string): string { + return Object.entries(variableLightness).filter(([key]) => key.search(regex) > -1) .map(([key, lightness]) => { - const lightnessOffset = lightness - variableLightness["--primary-600-hsl"]; + const lightnessOffset = lightness - variableLightness[centerVariable]; const plusOrMinus = lightnessOffset >= 0 ? "+" : "-"; return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`; }) .join("\n"); +} - const style = document.createElement("style"); - style.setAttribute("id", "clientThemeOffsets"); - style.textContent = `:root:root { - ${lightnessOffsets} - }`; - document.head.appendChild(style); + +function generateColorOffsets(styles) { + const variableLightness = {} as Record; + + // Get lightness values of --primary variables + let variableMatch = variableRegex.exec(styles); + while (variableMatch !== null) { + const [, variable, lightness] = variableMatch; + variableLightness[variable] = parseFloat(lightness); + variableMatch = variableRegex.exec(styles); + } + + createStyleSheet("clientThemeOffsets", [ + `.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`, + `.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`, + ].join("\n\n")); +} + +function generateLightModeFixes(styles) { + const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm; + // get light capturing groups that mention --white-500 + const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat(); + + const groupBackgroundRegex = /^([^{]*)\{background:var\(--white-500\)/m; + const groupBackgroundColorRegex = /^([^{]*)\{background-color:var\(--white-500\)/m; + // find all capturing groups that assign background or background-color directly to w500 + const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n"); + const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n"); + // create css to reassign them to --primary-100 + const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`; + const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`; + + const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m; + const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m; + // get all global variables used for backgrounds + const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500 + .map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[] + const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500 + // create css to reassign every var + const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`; + + createStyleSheet("clientThemeLightModeFixes", [ + reassignBackgrounds, + reassignBackgroundColors, + reassignVariables, + ].join("\n\n")); +} + +function captureOne(str, regex) { + const result = str.match(regex); + return (result === null) ? null : result[1]; +} + +function mapReject(arr, mapFunc, rejectFunc = _.isNull) { + return _.reject(arr.map(mapFunc), rejectFunc); } function updateColorVars(color: string) { const { hue, saturation, lightness } = hexToHSL(color); let style = document.getElementById("clientThemeVars"); - if (!style) { - style = document.createElement("style"); - style.setAttribute("id", "clientThemeVars"); - document.head.appendChild(style); - } + if (!style) + style = createStyleSheet("clientThemeVars"); style.textContent = `:root { --theme-h: ${hue}; @@ -156,6 +218,28 @@ function updateColorVars(color: string) { }`; } +function createStyleSheet(id, content = "") { + const style = document.createElement("style"); + style.setAttribute("id", id); + style.textContent = content.split("\n").map(line => line.trim()).join("\n"); + document.body.appendChild(style); + return style; +} + +// returns all of discord's native styles in a single string +async function getStyles(): Promise { + let out = ""; + const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); + for (const styleLinkNode of styleLinkNodes) { + const cssLink = styleLinkNode.getAttribute("href"); + if (!cssLink) continue; + + const res = await fetch(cssLink); + out += await res.text(); + } + return out; +} + // https://css-tricks.com/converting-color-spaces-in-javascript/ function hexToHSL(hexCode: string) { // Hex => RGB normalized to 0-1 @@ -198,17 +282,14 @@ function hexToHSL(hexCode: string) { return { hue, saturation, lightness }; } -// Minimized math just for lightness, lowers lag when changing colors -function hexToLightness(hexCode: string) { - // Hex => RGB normalized to 0-1 - const r = parseInt(hexCode.substring(0, 2), 16) / 255; - const g = parseInt(hexCode.substring(2, 4), 16) / 255; - const b = parseInt(hexCode.substring(4, 6), 16) / 255; +// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance +function relativeLuminance(hexCode: string) { + const normalize = (x: number) => + x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; - const cMax = Math.max(r, g, b); - const cMin = Math.min(r, g, b); + const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255); + const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255); + const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255); - const lightness = 100 * ((cMax + cMin) / 2); - - return lightness; + return r * 0.2126 + g * 0.7152 + b * 0.0722; } From 60bc823eab8876a63f8b3d192a703c99ace8921a Mon Sep 17 00:00:00 2001 From: Sam <149597648+cheesesamwich@users.noreply.github.com> Date: Tue, 16 Jan 2024 03:00:41 +0000 Subject: [PATCH 27/50] new plugin: BetterGifPicker (#2108) Co-authored-by: V --- src/plugins/betterGifPicker/index.ts | 23 +++++++++++++++++++++++ src/utils/constants.ts | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 src/plugins/betterGifPicker/index.ts diff --git a/src/plugins/betterGifPicker/index.ts b/src/plugins/betterGifPicker/index.ts new file mode 100644 index 000000000..09bb570d7 --- /dev/null +++ b/src/plugins/betterGifPicker/index.ts @@ -0,0 +1,23 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "BetterGifPicker", + description: "Makes the gif picker open the favourite category by default", + authors: [Devs.Samwich], + patches: [ + { + find: ".GIFPickerResultTypes.SEARCH", + replacement: [{ + match: "this.state={resultType:null}", + replace: 'this.state={resultType:"Favorites"}' + }] + } + ] +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index a94ba0fc3..699c40b90 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -403,6 +403,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Grzesiek11", id: 368475654662127616n, }, + Samwich: { + name: "Samwich", + id: 976176454511509554n, + }, } satisfies Record); // iife so #__PURE__ works correctly From 11d3165009d0a5d8a2f2594a88f60317f239b8a8 Mon Sep 17 00:00:00 2001 From: AutumnVN Date: Tue, 16 Jan 2024 10:00:53 +0700 Subject: [PATCH 28/50] roleColorEverywhere: thread role color (again) (#2098) Co-authored-by: V --- src/plugins/roleColorEverywhere/index.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index d718f4027..968027163 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -72,10 +72,6 @@ export default definePlugin({ { find: 'tutorialId:"whos-online', replacement: [ - { - match: /\i.roleIcon,\.\.\.\i/, - replace: "$&,color:$self.roleGroupColor(arguments[0])" - }, { match: /null,\i," — ",\i\]/, replace: "null,$self.roleGroupColor(arguments[0])]" @@ -83,6 +79,16 @@ export default definePlugin({ ], predicate: () => settings.store.memberList, }, + { + find: ".Messages.THREAD_BROWSER_PRIVATE", + replacement: [ + { + match: /children:\[\i," — ",\i\]/, + replace: "children:[$self.roleGroupColor(arguments[0])]" + }, + ], + predicate: () => settings.store.memberList, + }, { find: "renderPrioritySpeaker", replacement: [ From e8e09c17e9bb60b5c26ae862394d39faf5f6cd3b Mon Sep 17 00:00:00 2001 From: Bloofield <64427742+Bloofield@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:07:05 +1100 Subject: [PATCH 29/50] ClearURLs: Add igsh (#2103) --- src/plugins/clearURLs/defaultRules.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/clearURLs/defaultRules.ts b/src/plugins/clearURLs/defaultRules.ts index 0633b717d..95a59c037 100644 --- a/src/plugins/clearURLs/defaultRules.ts +++ b/src/plugins/clearURLs/defaultRules.ts @@ -153,5 +153,6 @@ export const defaultRules = [ "utm_term", "si@open.spotify.com", "igshid", + "igsh", "share_id@reddit.com", ]; From 8a168bd18571a08143bebb2bcd9c3807d19fc6d7 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 16 Jan 2024 04:12:15 +0100 Subject: [PATCH 30/50] bump to v1.6.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b373008e..076b2999d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.6", + "version": "1.6.7", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 3d64f3da410153447bc2fd49af2d7799af993e51 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 18 Jan 2024 00:39:34 +0100 Subject: [PATCH 31/50] WebContextMenus: fix copying images --- src/plugins/webContextMenus.web/index.ts | 67 ++++++++++++++++-------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index bb98c61d7..ec3070b11 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -46,6 +46,21 @@ const settings = definePluginSettings({ } }); +const MEDIA_PROXY_URL = "https://media.discordapp.net"; +const CDN_URL = "https://cdn.discordapp.com"; + +function fixImageUrl(urlString: string) { + const url = new URL(urlString); + if (url.origin === CDN_URL) return urlString; + if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname; + + url.searchParams.delete("width"); + url.searchParams.delete("height"); + url.searchParams.set("quality", "lossless"); + if (url.searchParams.get("format") === "webp") url.searchParams.set("format", "png"); + return url.toString(); +} + export default definePlugin({ name: "WebContextMenus", description: "Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)", @@ -182,34 +197,40 @@ export default definePlugin({ ], async copyImage(url: string) { - if (IS_VESKTOP && VesktopNative.clipboard) { - const data = await fetch(url).then(r => r.arrayBuffer()); - VesktopNative.clipboard.copyImage(data, url); - return; + url = fixImageUrl(url); + + let imageData = await fetch(url).then(r => r.blob()); + if (imageData.type !== "image/png") { + const bitmap = await createImageBitmap(imageData); + + const canvas = document.createElement("canvas"); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + canvas.getContext("2d")!.drawImage(bitmap, 0, 0); + + await new Promise(done => { + canvas.toBlob(data => { + imageData = data!; + done(); + }, "image/png"); + }); } - // Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png - // via canvas first - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = img.naturalWidth; - canvas.height = img.naturalHeight; - canvas.getContext("2d")!.drawImage(img, 0, 0); - - canvas.toBlob(data => { - navigator.clipboard.write([ - new ClipboardItem({ - "image/png": data! - }) - ]); - }, "image/png"); - }; - img.crossOrigin = "anonymous"; - img.src = url; + if (IS_VESKTOP && VesktopNative.clipboard) { + VesktopNative.clipboard.copyImage(await imageData.arrayBuffer(), url); + return; + } else { + navigator.clipboard.write([ + new ClipboardItem({ + "image/png": imageData + }) + ]); + } }, async saveImage(url: string) { + url = fixImageUrl(url); + const data = await fetchImage(url); if (!data) return; From 74300e0a6963dec7f83dc9f21f93736be9990b59 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 18 Jan 2024 00:58:40 +0100 Subject: [PATCH 32/50] WebContextMenus: only force png on copy image, not save image --- src/plugins/webContextMenus.web/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index ec3070b11..5f6beca2c 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -49,7 +49,7 @@ const settings = definePluginSettings({ const MEDIA_PROXY_URL = "https://media.discordapp.net"; const CDN_URL = "https://cdn.discordapp.com"; -function fixImageUrl(urlString: string) { +function fixImageUrl(urlString: string, explodeWebp: boolean) { const url = new URL(urlString); if (url.origin === CDN_URL) return urlString; if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname; @@ -57,7 +57,9 @@ function fixImageUrl(urlString: string) { url.searchParams.delete("width"); url.searchParams.delete("height"); url.searchParams.set("quality", "lossless"); - if (url.searchParams.get("format") === "webp") url.searchParams.set("format", "png"); + if (explodeWebp && url.searchParams.get("format") === "webp") + url.searchParams.set("format", "png"); + return url.toString(); } @@ -197,7 +199,7 @@ export default definePlugin({ ], async copyImage(url: string) { - url = fixImageUrl(url); + url = fixImageUrl(url, true); let imageData = await fetch(url).then(r => r.blob()); if (imageData.type !== "image/png") { @@ -229,7 +231,7 @@ export default definePlugin({ }, async saveImage(url: string) { - url = fixImageUrl(url); + url = fixImageUrl(url, false); const data = await fetchImage(url); if (!data) return; From 988435714e8b1de6e7e95885cbb5222113fb039c Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 19 Jan 2024 01:08:25 +0100 Subject: [PATCH 33/50] Add back transparency option --- src/components/VencordSettings/VencordTab.tsx | 6 +++--- src/main/patcher.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 07d777eb3..ab910ea2a 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -83,10 +83,10 @@ function VencordSettings() { title: "Use Windows' native title bar instead of Discord's custom one", note: "Requires a full restart" }), - !IS_WEB && false /* This causes electron to freeze / white screen for some people */ && { + !IS_WEB && { key: "transparent", - title: "Enable window transparency", - note: "Requires a full restart" + title: "Enable window transparency.", + note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart" }, !IS_WEB && isWindows && { key: "winCtrlQ", diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 0cc92550c..76d1ccaf3 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -79,8 +79,7 @@ if (!IS_VANILLA) { delete options.frame; } - // This causes electron to freeze / white screen for some people - if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) { + if (settings.transparent) { options.transparent = true; options.backgroundColor = "#00000000"; } From 16707334582151df832743042ba6189936048ee7 Mon Sep 17 00:00:00 2001 From: ~coolelectronics Date: Thu, 18 Jan 2024 20:07:35 -0500 Subject: [PATCH 34/50] feat(plugin): FixYoutubeEmbeds - fix UMG blocked embeds (#2116) Co-authored-by: V --- .../fixYoutubeEmbeds.desktop/README.md | 5 ++++ src/plugins/fixYoutubeEmbeds.desktop/index.ts | 14 ++++++++++ .../fixYoutubeEmbeds.desktop/native.ts | 26 +++++++++++++++++++ src/utils/constants.ts | 4 +++ 4 files changed, 49 insertions(+) create mode 100644 src/plugins/fixYoutubeEmbeds.desktop/README.md create mode 100644 src/plugins/fixYoutubeEmbeds.desktop/index.ts create mode 100644 src/plugins/fixYoutubeEmbeds.desktop/native.ts diff --git a/src/plugins/fixYoutubeEmbeds.desktop/README.md b/src/plugins/fixYoutubeEmbeds.desktop/README.md new file mode 100644 index 000000000..875de9e28 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/README.md @@ -0,0 +1,5 @@ +# FixYoutubeEmbeds + +Bypasses youtube videos being blocked from display on Discord (for example by UMG) + +![](https://github.com/Vendicated/Vencord/assets/45497981/7a5fdcaa-217c-4c63-acae-f0d6af2f79be) diff --git a/src/plugins/fixYoutubeEmbeds.desktop/index.ts b/src/plugins/fixYoutubeEmbeds.desktop/index.ts new file mode 100644 index 000000000..55486d763 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/index.ts @@ -0,0 +1,14 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "FixYoutubeEmbeds", + description: "Bypasses youtube videos being blocked from display on Discord (for example by UMG)", + authors: [Devs.coolelectronics] +}); diff --git a/src/plugins/fixYoutubeEmbeds.desktop/native.ts b/src/plugins/fixYoutubeEmbeds.desktop/native.ts new file mode 100644 index 000000000..5a3ef2c62 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/native.ts @@ -0,0 +1,26 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { app } from "electron"; +import { getSettings } from "main/ipcMain"; + +app.on("browser-window-created", (_, win) => { + win.webContents.on("frame-created", (_, { frame }) => { + frame.once("dom-ready", () => { + if (frame.url.startsWith("https://www.youtube.com/")) { + const settings = getSettings().plugins?.FixYoutubeEmbeds; + if (!settings?.enabled) return; + + frame.executeJavaScript(` + new MutationObserver(() => { + let err = document.querySelector(".ytp-error-content-wrap-subreason span")?.textContent; + if (err && err.includes("blocked it from display")) window.location.reload() + }).observe(document.body, { childList: true, subtree:true }); + `); + } + }); + }); +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 699c40b90..899936128 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -407,6 +407,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Samwich", id: 976176454511509554n, }, + coolelectronics: { + name: "coolelectronics", + id: 696392247205298207n, + } } satisfies Record); // iife so #__PURE__ works correctly From e707538b73ee8e2430c09a476904ea41ab1b7979 Mon Sep 17 00:00:00 2001 From: Manti <67705577+mantikafasi@users.noreply.github.com> Date: Mon, 22 Jan 2024 03:18:48 +0300 Subject: [PATCH 35/50] [ReviewDB] update for new api changes; some fixes (#2120) Co-authored-by: Vendicated --- src/plugins/reviewDB/auth.tsx | 17 +++-- .../reviewDB/components/ReviewComponent.tsx | 17 +++-- .../reviewDB/components/ReviewsView.tsx | 8 +-- src/plugins/reviewDB/index.tsx | 3 +- src/plugins/reviewDB/reviewDbApi.ts | 66 ++++++++++--------- src/plugins/reviewDB/settings.tsx | 63 +++++++++--------- src/plugins/reviewDB/style.css | 23 ++++++- src/plugins/reviewDB/utils.tsx | 13 +++- 8 files changed, 126 insertions(+), 84 deletions(-) diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx index e7a369217..136001b2d 100644 --- a/src/plugins/reviewDB/auth.tsx +++ b/src/plugins/reviewDB/auth.tsx @@ -61,14 +61,17 @@ export function authorize(callback?: any) { const res = await fetch(url, { headers: new Headers({ Accept: "application/json" }) }); - const { token, success } = await res.json(); - if (success) { - updateAuth({ token }); - showToast("Successfully logged in!", Toasts.Type.SUCCESS); - callback?.(); - } else if (res.status === 1) { - showToast("An Error occurred while logging in.", Toasts.Type.FAILURE); + + if (!res.ok) { + const { message } = await res.json(); + showToast(message || "An error occured while authorizing", Toasts.Type.FAILURE); + return; } + + const { token } = await res.json(); + updateAuth({ token }); + showToast("Successfully logged in!", Toasts.Type.SUCCESS); + callback?.(); } catch (e) { new Logger("ReviewDB").error("Failed to authorize", e); } diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx index 13b0f167f..977745a25 100644 --- a/src/plugins/reviewDB/components/ReviewComponent.tsx +++ b/src/plugins/reviewDB/components/ReviewComponent.tsx @@ -20,13 +20,13 @@ import { openUserProfile } from "@utils/discord"; import { classes } from "@utils/misc"; import { LazyComponent } from "@utils/react"; import { filters, findBulk } from "@webpack"; -import { Alerts, moment, Parser, showToast, Timestamp } from "@webpack/common"; +import { Alerts, moment, Parser, Timestamp, useState } from "@webpack/common"; import { Auth, getToken } from "../auth"; import { Review, ReviewType } from "../entities"; import { blockUser, deleteReview, reportReview, unblockUser } from "../reviewDbApi"; import { settings } from "../settings"; -import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl } from "../utils"; +import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl, showToast } from "../utils"; import { openBlockModal } from "./BlockedUserModal"; import { BlockButton, DeleteButton, ReportButton } from "./MessageButton"; import ReviewBadge from "./ReviewBadge"; @@ -51,6 +51,8 @@ export default LazyComponent(() => { const dateFormat = new Intl.DateTimeFormat(); return function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) { + const [showAll, setShowAll] = useState(false); + function openModal() { openUserProfile(review.sender.discordID); } @@ -66,10 +68,9 @@ export default LazyComponent(() => { return showToast("You must be logged in to delete reviews."); } else { deleteReview(review.id).then(res => { - if (res.success) { + if (res) { refetch(); } - showToast(res.message); }); } } @@ -116,11 +117,11 @@ export default LazyComponent(() => { } return ( -
@@ -168,7 +169,9 @@ export default LazyComponent(() => { }
- {Parser.parseGuildEventDescription(review.comment)} + {(review.comment.length > 200 && !showAll) + ? [Parser.parseGuildEventDescription(review.comment.substring(0, 200)), "...",
, ( setShowAll(true)}>Read more)] + : Parser.parseGuildEventDescription(review.comment)}
{review.id !== 0 && ( diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index cfd5477db..64cea1815 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -18,13 +18,13 @@ import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react"; import { find, findByPropsLazy } from "@webpack"; -import { Forms, React, RelationshipStore, showToast, useRef, UserStore } from "@webpack/common"; +import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common"; import { Auth, authorize } from "../auth"; import { Review } from "../entities"; import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; import { settings } from "../settings"; -import { cl } from "../utils"; +import { cl, showToast } from "../utils"; import ReviewComponent from "./ReviewComponent"; @@ -168,7 +168,7 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { comment: res.value, }); - if (response?.success) { + if (response) { refetch(); const slateEditor = editorRef.current.ref.current.getSlateEditor(); @@ -180,8 +180,6 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { focus: Editor.end(slateEditor, []), } }); - } else if (response?.message) { - showToast(response.message); } // even tho we need to return this, it doesnt do anything diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index d8357faf2..50bb62184 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -25,7 +25,7 @@ import { OpenExternalIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin from "@utils/types"; -import { Alerts, Menu, Parser, showToast, useState } from "@webpack/common"; +import { Alerts, Menu, Parser, useState } from "@webpack/common"; import { Guild, User } from "discord-types/general"; import { Auth, initAuth, updateAuth } from "./auth"; @@ -34,6 +34,7 @@ import ReviewsView from "./components/ReviewsView"; import { NotificationType } from "./entities"; import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { settings } from "./settings"; +import { showToast } from "./utils"; const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => { children.push( diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts index 657e9c475..ec6d9ff3b 100644 --- a/src/plugins/reviewDB/reviewDbApi.ts +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -16,18 +16,18 @@ * along with this program. If not, see . */ -import { showToast, Toasts } from "@webpack/common"; +import { Toasts } from "@webpack/common"; import { Auth, authorize, getToken, updateAuth } from "./auth"; import { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from "./entities"; import { settings } from "./settings"; +import { showToast } from "./utils"; const API_URL = "https://manti.vendicated.dev/api/reviewdb"; export const REVIEWS_PER_PAGE = 50; export interface Response { - success: boolean, message: string; reviews: Review[]; updated: boolean; @@ -37,6 +37,16 @@ export interface Response { const WarningFlag = 0b00000010; +async function rdbRequest(path: string, options: RequestInit = {}) { + return fetch(API_URL + path, { + ...options, + headers: { + ...options.headers, + Authorization: await getToken() || "", + } + }); +} + export async function getReviews(id: string, offset = 0): Promise { let flags = 0; if (!settings.store.showWarning) flags |= WarningFlag; @@ -47,10 +57,9 @@ export async function getReviews(id: string, offset = 0): Promise { }); const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`); - const res = (req.status === 200) + const res = (req.ok) ? await req.json() as Response : { - success: false, message: req.status === 429 ? "You are sending requests too fast. Wait a few seconds and try again." : "An Error occured while fetching reviews. Please try again later.", reviews: [], updated: false, @@ -58,7 +67,7 @@ export async function getReviews(id: string, offset = 0): Promise { reviewCount: 0 }; - if (!res.success) { + if (!req.ok) { showToast(res.message, Toasts.Type.FAILURE); return { ...res, @@ -85,44 +94,46 @@ export async function getReviews(id: string, offset = 0): Promise { } export async function addReview(review: any): Promise { - review.token = await getToken(); - if (!review.token) { + const token = await getToken(); + if (!token) { showToast("Please authorize to add a review."); authorize(); return null; } - return fetch(API_URL + `/users/${review.userid}/reviews`, { + return await rdbRequest(`/users/${review.userid}/reviews`, { method: "PUT", body: JSON.stringify(review), headers: { "Content-Type": "application/json", } - }) - .then(r => r.json()) - .then(res => { - showToast(res.message); - return res ?? null; - }); + }).then(async r => { + const data = await r.json() as Response; + showToast(data.message); + return r.ok ? data : null; + }); } -export async function deleteReview(id: number): Promise { - return fetch(API_URL + `/users/${id}/reviews`, { +export async function deleteReview(id: number): Promise { + return await rdbRequest(`/users/${id}/reviews`, { method: "DELETE", headers: new Headers({ "Content-Type": "application/json", Accept: "application/json", }), body: JSON.stringify({ - token: await getToken(), reviewid: id }) - }).then(r => r.json()); + }).then(async r => { + const data = await r.json() as Response; + showToast(data.message); + return r.ok ? data : null; + }); } export async function reportReview(id: number) { - const res = await fetch(API_URL + "/reports", { + const res = await rdbRequest("/reports", { method: "PUT", headers: new Headers({ "Content-Type": "application/json", @@ -130,7 +141,6 @@ export async function reportReview(id: number) { }), body: JSON.stringify({ reviewid: id, - token: await getToken() }) }).then(r => r.json()) as Response; @@ -138,12 +148,11 @@ export async function reportReview(id: number) { } async function patchBlock(action: "block" | "unblock", userId: string) { - const res = await fetch(API_URL + "/blocks", { + const res = await rdbRequest("/blocks", { method: "PATCH", headers: new Headers({ "Content-Type": "application/json", Accept: "application/json", - Authorization: await getToken() || "" }), body: JSON.stringify({ action: action, @@ -169,11 +178,10 @@ export const blockUser = (userId: string) => patchBlock("block", userId); export const unblockUser = (userId: string) => patchBlock("unblock", userId); export async function fetchBlocks(): Promise { - const res = await fetch(API_URL + "/blocks", { + const res = await rdbRequest("/blocks", { method: "GET", headers: new Headers({ Accept: "application/json", - Authorization: await getToken() || "" }) }); @@ -182,17 +190,13 @@ export async function fetchBlocks(): Promise { } export function getCurrentUserInfo(token: string): Promise { - return fetch(API_URL + "/users", { - body: JSON.stringify({ token }), + return rdbRequest("/users", { method: "POST", }).then(r => r.json()); } export async function readNotification(id: number) { - return fetch(API_URL + `/notifications?id=${id}`, { - method: "PATCH", - headers: { - "Authorization": await getToken() || "", - }, + return rdbRequest(`/notifications?id=${id}`, { + method: "PATCH" }); } diff --git a/src/plugins/reviewDB/settings.tsx b/src/plugins/reviewDB/settings.tsx index efcb80588..79cf4174e 100644 --- a/src/plugins/reviewDB/settings.tsx +++ b/src/plugins/reviewDB/settings.tsx @@ -22,13 +22,14 @@ import { Button } from "@webpack/common"; import { authorize, getToken } from "./auth"; import { openBlockModal } from "./components/BlockedUserModal"; +import { cl } from "./utils"; export const settings = definePluginSettings({ authorize: { type: OptionType.COMPONENT, description: "Authorize with ReviewDB", component: () => ( - ) @@ -53,38 +54,40 @@ export const settings = definePluginSettings({ description: "Hide reviews from blocked users", default: true, }, - manageBlocks: { + buttons: { type: OptionType.COMPONENT, - description: "Manage Blocked Users", + description: "ReviewDB buttons", component: () => ( - - ) - }, - website: { - type: OptionType.COMPONENT, - description: "ReviewDB website", - component: () => ( - - VencordNative.native.openExternal(url); - }}> - ReviewDB website - - ) - }, - supportServer: { - type: OptionType.COMPONENT, - description: "ReviewDB Support Server", - component: () => ( - + + + + + + +
) } }).withPrivateSettings<{ diff --git a/src/plugins/reviewDB/style.css b/src/plugins/reviewDB/style.css index a812ecaf2..190b8f620 100644 --- a/src/plugins/reviewDB/style.css +++ b/src/plugins/reviewDB/style.css @@ -59,8 +59,14 @@ } .vc-rdb-review { - margin-top: 8px; - margin-bottom: 8px; + padding-top: 8px !important; + padding-bottom: 8px !important; + padding-right: 32px !important; +} + +.vc-rdb-review:hover { + background: var(--background-message-hover) !important; + border-radius: 8px; } .vc-rdb-review-comment img { @@ -91,6 +97,19 @@ gap: 0.75em; } +.vc-rdb-button-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +/* stylelint-disable-next-line media-feature-range-notation */ +@media (max-width: 600px) { + .vc-rdb-button-grid { + grid-template-columns: 1fr; + } +} + .vc-rdb-block-modal-row { display: flex; height: 2em; diff --git a/src/plugins/reviewDB/utils.tsx b/src/plugins/reviewDB/utils.tsx index eeaca1204..916adc45e 100644 --- a/src/plugins/reviewDB/utils.tsx +++ b/src/plugins/reviewDB/utils.tsx @@ -17,7 +17,7 @@ */ import { classNameFactory } from "@api/Styles"; -import { UserStore } from "@webpack/common"; +import { Toasts, UserStore } from "@webpack/common"; import { Auth } from "./auth"; import { Review, UserType } from "./entities"; @@ -41,3 +41,14 @@ export function canBlockReviewAuthor(profileId: string, review: Review) { export function canReportReview(review: Review) { return review.sender.discordID !== UserStore.getCurrentUser().id; } + +export function showToast(message: string, type = Toasts.Type.MESSAGE) { + Toasts.show({ + id: Toasts.genId(), + message, + type, + options: { + position: Toasts.Position.BOTTOM, // NOBODY LIKES TOASTS AT THE TOP + }, + }); +} From 4bb0db506682bf03b66366e70ae249979b95f695 Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Mon, 22 Jan 2024 01:21:10 +0100 Subject: [PATCH 36/50] fix anonymiseFileNames (#2125) --- src/plugins/anonymiseFileNames/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index 845aa756d..0382b65c2 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -81,7 +81,7 @@ export default definePlugin({ { find: ".Messages.ATTACHMENT_UTILITIES_SPOILER", replacement: { - match: /(?<=children:\[)(?=.{10,80}tooltip:\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/, + match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/, replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null," }, }, From 620c127b58e94ce4bf0b78d7e55fbce8e4f376e2 Mon Sep 17 00:00:00 2001 From: V Date: Thu, 25 Jan 2024 07:22:34 +0100 Subject: [PATCH 37/50] [skip ci] fix pnpm inject --- scripts/runInstaller.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/runInstaller.mjs b/scripts/runInstaller.mjs index b35039f8a..8b2510b2e 100644 --- a/scripts/runInstaller.mjs +++ b/scripts/runInstaller.mjs @@ -35,11 +35,11 @@ const ETAG_FILE = join(FILE_DIR, "etag.txt"); function getFilename() { switch (process.platform) { case "win32": - return "VencordInstaller.exe"; + return "VencordInstallerCli.exe"; case "darwin": return "VencordInstaller.MacOS.zip"; case "linux": - return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11"); + return "VencordInstallerCli-linux"; default: throw new Error("Unsupported platform: " + process.platform); } From cc885b5bb367add1c4e831ab1a4ab6145f6b0f5c Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 3 Feb 2024 02:11:59 +0100 Subject: [PATCH 38/50] remove lumap --- src/plugins/pictureInPicture/index.tsx | 2 +- src/utils/constants.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/pictureInPicture/index.tsx b/src/plugins/pictureInPicture/index.tsx index ba4aa8387..ca766affc 100644 --- a/src/plugins/pictureInPicture/index.tsx +++ b/src/plugins/pictureInPicture/index.tsx @@ -24,7 +24,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "PictureInPicture", description: "Adds picture in picture to videos (next to the Download button)", - authors: [Devs.Lumap], + authors: [Devs.Nobody], settings, patches: [ { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 899936128..55af93605 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -42,6 +42,10 @@ export interface Dev { * If you are fine with attribution but don't want the badge, add badge: false */ export const Devs = /* #__PURE__*/ Object.freeze({ + Nobody: { + name: "Nobody", + id: 0n, + }, Ven: { name: "Vendicated", id: 343383572805058560n @@ -359,10 +363,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "bb010g", id: 72791153467990016n, }, - Lumap: { - name: "lumap", - id: 635383782576357407n - }, Dolfies: { name: "Dolfies", id: 852892297661906993n, From 8938f4a3cf9a36aa490703651934acccbd07bdf9 Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Sun, 4 Feb 2024 01:50:51 +0100 Subject: [PATCH 39/50] fix moreUserTags (#2146) --- src/plugins/moreUserTags/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx index 9921bca8e..d1ad941b0 100644 --- a/src/plugins/moreUserTags/index.tsx +++ b/src/plugins/moreUserTags/index.tsx @@ -198,7 +198,7 @@ export default definePlugin({ replacement: [ // make the tag show the right text { - match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(\i\.\i\.Messages)\.BOT_TAG_BOT/, + match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.BOT_TAG_BOT/, replace: (_, origSwitch, variant, tags, displayedText, strings) => `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}` }, From bf977e0047141dd479b26f66cc848923310bcb6b Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 6 Feb 2024 16:29:47 +0100 Subject: [PATCH 40/50] Add chat bar button api ~ fixes buttons for russian users --- src/api/ChatButtons.tsx | 123 ++++++++++++++++++++ src/api/index.ts | 6 + src/plugins/_api/chatButtons.ts | 22 ++++ src/plugins/invisibleChat.desktop/index.tsx | 88 +++++--------- src/plugins/previewMessage/index.tsx | 74 +++++------- src/plugins/sendTimestamps/index.tsx | 101 +++++++--------- src/plugins/silentMessageToggle/index.tsx | 78 +++++-------- src/plugins/silentTyping/index.tsx | 66 ++++------- src/plugins/translate/TranslateIcon.tsx | 52 ++++----- src/plugins/translate/index.tsx | 22 +--- 10 files changed, 325 insertions(+), 307 deletions(-) create mode 100644 src/api/ChatButtons.tsx create mode 100644 src/plugins/_api/chatButtons.ts diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx new file mode 100644 index 000000000..0350965af --- /dev/null +++ b/src/api/ChatButtons.tsx @@ -0,0 +1,123 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Logger } from "@utils/Logger"; +import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; +import { Channel } from "discord-types/general"; +import { HTMLProps, MouseEventHandler, ReactNode } from "react"; + +export interface ChatBarProps { + channel: Channel; + disabled: boolean; + isEmpty: boolean; + type: { + analyticsName: string; + attachments: boolean; + autocomplete: { + addReactionShortcut: boolean, + forceChatLayer: boolean, + reactions: boolean; + }, + commands: { + enabled: boolean; + }, + drafts: { + type: number, + commandType: number, + autoSave: boolean; + }, + emojis: { + button: boolean; + }, + gifs: { + button: boolean, + allowSending: boolean; + }, + gifts: { + button: boolean; + }, + permissions: { + requireSendMessages: boolean; + }, + showThreadPromptOnReply: boolean, + stickers: { + button: boolean, + allowSending: boolean, + autoSuggest: boolean; + }, + users: { + allowMentioning: boolean; + }, + submit: { + button: boolean, + ignorePreference: boolean, + disableEnterToSubmit: boolean, + clearOnSubmit: boolean, + useDisabledStylesOnSubmit: boolean; + }, + uploadLongMessages: boolean, + upsellLongMessages: { + iconOnly: boolean; + }, + showCharacterCount: boolean, + sedReplace: boolean; + }; +} + +export type ChatBarButton = (props: ChatBarProps, isMainChat: boolean) => ReactNode; + +const buttonFactories = new Map(); +const logger = new Logger("ChatButtons"); + +export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) { + if (props.type.analyticsName !== "normal") return; + + for (const [key, makeButton] of buttonFactories) { + try { + const res = makeButton(props, props.type.analyticsName === "normal"); + if (res) buttons.push(res); + } catch (e) { + logger.error(`Failed to render button ${key}`, e); + } + } +} + +export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button); +export const removeChatBarButton = (id: string) => buttonFactories.delete(id); + +export interface ChatBarButtonProps { + children: ReactNode; + tooltip: string; + onClick: MouseEventHandler; + onContextMenu?: MouseEventHandler; + buttonProps?: Omit, "size" | "onClick" | "onContextMenu">; +} +export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { + return ( + + {({ onMouseEnter, onMouseLeave }) => ( +
+ +
+ )} +
+ ); +}, { noop: true }); diff --git a/src/api/index.ts b/src/api/index.ts index 08f238104..5dca63105 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -17,6 +17,7 @@ */ import * as $Badges from "./Badges"; +import * as $ChatButtons from "./ChatButtons"; import * as $Commands from "./Commands"; import * as $ContextMenu from "./ContextMenu"; import * as $DataStore from "./DataStore"; @@ -104,3 +105,8 @@ export const Notifications = $Notifications; * An api allowing you to patch and add/remove items to/from context menus */ export const ContextMenu = $ContextMenu; + +/** + * An API allowing you to add buttons to the chat input + */ +export const ChatButtons = $ChatButtons; diff --git a/src/plugins/_api/chatButtons.ts b/src/plugins/_api/chatButtons.ts new file mode 100644 index 000000000..ca85964c0 --- /dev/null +++ b/src/plugins/_api/chatButtons.ts @@ -0,0 +1,22 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "ChatInputButtonAPI", + description: "API to add buttons to the chat input", + authors: [Devs.Ven], + + patches: [{ + find: 'location:"ChannelTextAreaButtons"', + replacement: { + match: /if\(!\i\.isMobile\)\{(?=.+?&&(\i)\.push\(.{0,50}"gift")/, + replace: "$&Vencord.Api.ChatButtons._injectButtons($1,arguments[0]);" + } + }] +}); diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx index c80c4ce54..3184e025d 100644 --- a/src/plugins/invisibleChat.desktop/index.tsx +++ b/src/plugins/invisibleChat.desktop/index.tsx @@ -16,13 +16,14 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getStegCloak } from "@utils/dependencies"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; import { buildDecModal } from "./components/DecryptionModal"; @@ -64,54 +65,32 @@ function Indicator() { } -function ChatBarIcon(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - if (chatBoxProps.type.analyticsName !== "normal") return null; +const ChatBarIcon: ChatBarButton = (_, isMainChat) => { + if (!isMainChat) return null; return ( - - {({ onMouseEnter, onMouseLeave }) => ( - // size="" = Button.Sizes.NONE - /* - many themes set "> button" to display: none, as the gift button is - the only directly descending button (all the other elements are divs.) - Thus, wrap in a div here to avoid getting hidden by that. - flex is for some reason necessary as otherwise the button goes flying off - */ -
- -
- ) - } -
+ buildEncModal()} + + buttonProps={{ + "aria-haspopup": "dialog", + style: { padding: "0 2px", scale: "0.9" } + }} + > + + + + ); -} +}; const settings = definePluginSettings({ savedPasswords: { @@ -125,7 +104,7 @@ export default definePlugin({ name: "InvisibleChat", description: "Encrypt your Messages in a non-suspicious way!", authors: [Devs.SammCheese], - dependencies: ["MessagePopoverAPI"], + dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"], patches: [ { // Indicator @@ -135,13 +114,6 @@ export default definePlugin({ replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&" } }, - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, ], EMBED_API_URL: "https://embed.sammcheese.net", @@ -154,7 +126,7 @@ export default definePlugin({ const { default: StegCloak } = await getStegCloak(); steggo = new StegCloak(true, false); - addButton("invDecrypt", message => { + addButton("InvisibleChat", message => { return this.INV_REGEX.test(message?.content) ? { label: "Decrypt Message", @@ -170,10 +142,13 @@ export default definePlugin({ } : null; }); + + addChatBarButton("InvisibleChat", ChatBarIcon); }, stop() { - removeButton("invDecrypt"); + removeButton("InvisibleChat"); + removeButton("InvisibleChat"); }, // Gets the Embed of a Link @@ -216,7 +191,6 @@ export default definePlugin({ }); }, - chatBarIcon: ErrorBoundary.wrap(ChatBarIcon, { noop: true }), popOverIcon: () => , indicator: ErrorBoundary.wrap(Indicator, { noop: true }) }); diff --git a/src/plugins/previewMessage/index.tsx b/src/plugins/previewMessage/index.tsx index f2634ae6b..1d8b769d5 100644 --- a/src/plugins/previewMessage/index.tsx +++ b/src/plugins/previewMessage/index.tsx @@ -16,22 +16,14 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { generateId, sendBotMessage } from "@api/Commands"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; +import { DraftStore, DraftType, SelectedChannelStore, UserStore, useStateFromStores } from "@webpack/common"; import { MessageAttachment } from "discord-types/general"; -interface Props { - type: { - analyticsName: string; - isEmpty: boolean; - attachments: boolean; - }; -} - const UploadStore = findByPropsLazy("getUploads"); const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); @@ -81,13 +73,13 @@ const getAttachments = async (channelId: string) => ); -export function PreviewButton(chatBoxProps: Props) { - const { isEmpty, attachments } = chatBoxProps.type; +const PreviewButton: ChatBarButton = (props, isMainChat) => { + const { isEmpty, type: { attachments } } = props; const channelId = SelectedChannelStore.getChannelId(); const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat) return null; const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0; const hasContent = !isEmpty && draft?.length > 0; @@ -95,47 +87,33 @@ export function PreviewButton(chatBoxProps: Props) { if (!hasContent && !hasAttachments) return null; return ( - - {tooltipProps => ( - - )} - + + sendBotMessage( + channelId, + { + content: getDraft(channelId), + author: UserStore.getCurrentUser(), + attachments: hasAttachments ? await getAttachments(channelId) : undefined, + } + )} + buttonProps={{ + style: { padding: "0 2px", height: "100%" } + }} + > + + ); -} +}; export default definePlugin({ name: "PreviewMessage", description: "Lets you preview your message before sending it.", authors: [Devs.Aria], - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], + dependencies: ["ChatInputButtonAPI"], - chatBarIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }), + start: () => addChatBarButton("previewMessage", PreviewButton), + stop: () => removeChatBarButton("previewMessage"), }); diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx index 6d488add6..bd888a82b 100644 --- a/src/plugins/sendTimestamps/index.tsx +++ b/src/plugins/sendTimestamps/index.tsx @@ -18,6 +18,7 @@ import "./styles.css"; +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; @@ -26,7 +27,7 @@ import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, Forms, Parser, Select, Tooltip, useMemo, useState } from "@webpack/common"; +import { Button, Forms, Parser, Select, useMemo, useState } from "@webpack/common"; const settings = definePluginSettings({ replaceMessageContents: { @@ -122,25 +123,51 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi ); } +const ChatBarIcon: ChatBarButton = (_, isMainChat) => { + if (!isMainChat) return null; + + return ( + { + const key = openModal(props => ( + closeModal(key)} + /> + )); + }} + buttonProps={{ + "aria-haspopup": "dialog", + className: cl("button") + }} + > + + + ); +}; + export default definePlugin({ name: "SendTimestamps", description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!", authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11], - dependencies: ["MessageEventsAPI"], + dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], - settings: settings, - - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], + settings, start() { + addChatBarButton("SendTimestamps", ChatBarIcon); this.listener = addPreSendListener((_, msg) => { if (settings.store.replaceMessageContents) { msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); @@ -149,56 +176,10 @@ export default definePlugin({ }, stop() { + removeChatBarButton("SendTimestamps"); removePreSendListener(this.listener); }, - chatBarIcon(chatBoxProps: { type: { analyticsName: string; }; }) { - if (chatBoxProps.type.analyticsName !== "normal") return null; - - return ( - - {({ onMouseEnter, onMouseLeave }) => ( -
- -
- ) - } -
- ); - }, - settingsAboutComponent() { const samples = [ "12:00", diff --git a/src/plugins/silentMessageToggle/index.tsx b/src/plugins/silentMessageToggle/index.tsx index b7b33826d..6c7d179be 100644 --- a/src/plugins/silentMessageToggle/index.tsx +++ b/src/plugins/silentMessageToggle/index.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, React, Tooltip } from "@webpack/common"; +import { React, useEffect, useState } from "@webpack/common"; let lastState = false; @@ -41,19 +41,15 @@ const settings = definePluginSettings({ } }); -function SilentMessageToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const [enabled, setEnabled] = React.useState(lastState); +const SilentMessageToggle: ChatBarButton = (_, isMainChat) => { + const [enabled, setEnabled] = useState(lastState); function setEnabledValue(value: boolean) { if (settings.store.persistState) lastState = value; setEnabled(value); } - React.useEffect(() => { + useEffect(() => { const listener: SendListener = (_, message) => { if (enabled) { if (settings.store.autoDisable) setEnabledValue(false); @@ -65,55 +61,37 @@ function SilentMessageToggle(chatBoxProps: { return () => void removePreSendListener(listener); }, [enabled]); - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat) return null; return ( - - {tooltipProps => ( -
- -
- )} -
+ setEnabledValue(!enabled)} + buttonProps={{ + style: { padding: "0 6px" } + }} + > + + + {!enabled && <> + + + + + + } + + ); -} +}; export default definePlugin({ name: "SilentMessageToggle", authors: [Devs.Nuckyz, Devs.CatNoir], description: "Adds a button to the chat bar to toggle sending a silent message.", - dependencies: ["MessageEventsAPI"], - + dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], settings, - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], - chatBarIcon: ErrorBoundary.wrap(SilentMessageToggle, { noop: true }), + start: () => addChatBarButton("SilentMessageToggle", SilentMessageToggle), + stop: () => removeChatBarButton("SilentMessageToggle") }); diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index dae7ad4c9..1d336b477 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, React, Tooltip } from "@webpack/common"; +import { FluxDispatcher, React } from "@webpack/common"; const settings = definePluginSettings({ showIcon: { @@ -37,45 +37,35 @@ const settings = definePluginSettings({ } }); -function SilentTypingToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const { isEnabled } = settings.use(["isEnabled"]); +const SilentTypingToggle: ChatBarButton = (_, isMainChat) => { + const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]); const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat || !showIcon) return null; return ( - - {(tooltipProps: any) => ( -
- -
- )} -
+ + + + {isEnabled && } + + ); -} +}; export default definePlugin({ name: "SilentTyping", authors: [Devs.Ven, Devs.Rini], description: "Hide that you are typing", + dependencies: ["CommandsAPI", "ChatInputButtonAPI"], + settings, + patches: [ { find: '.dispatch({type:"TYPING_START_LOCAL"', @@ -84,17 +74,8 @@ export default definePlugin({ replace: "startTyping:$self.startTyping,stop" } }, - { - find: "ChannelTextAreaButtons", - predicate: () => settings.store.showIcon, - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, ], - dependencies: ["CommandsAPI"], - settings, + commands: [{ name: "silenttype", description: "Toggle whether you're hiding that you're typing or not.", @@ -120,5 +101,6 @@ export default definePlugin({ FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); }, - chatBarIcon: ErrorBoundary.wrap(SilentTypingToggle, { noop: true }), + start: () => addChatBarButton("SilentTyping", SilentTypingToggle), + stop: () => removeChatBarButton("SilentTyping"), }); diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx index 649589435..a7d789279 100644 --- a/src/plugins/translate/TranslateIcon.tsx +++ b/src/plugins/translate/TranslateIcon.tsx @@ -16,9 +16,9 @@ * along with this program. If not, see . */ +import { ChatBarButton } from "@api/ChatButtons"; import { classes } from "@utils/misc"; import { openModal } from "@utils/modal"; -import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; import { settings } from "./settings"; import { TranslateModal } from "./TranslateModal"; @@ -37,42 +37,30 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?: ); } -export function TranslateChatBarIcon({ slateProps }: { slateProps: { type: { analyticsName: string; }; }; }) { +export const TranslateChatBarIcon: ChatBarButton = (props, isMainChat) => { const { autoTranslate } = settings.use(["autoTranslate"]); - if (slateProps.type.analyticsName !== "normal") - return null; + if (!isMainChat) return null; const toggle = () => settings.store.autoTranslate = !autoTranslate; return ( - - {({ onMouseEnter, onMouseLeave }) => ( -
- -
- )} -
+ openModal(props => ( + + )); + }} + onContextMenu={() => toggle()} + buttonProps={{ + "aria-haspopup": "dialog", + style: { padding: "0 4px" } + }} + > + + ); -} +}; diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index 3b067c634..702e60cf7 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -18,11 +18,11 @@ import "./styles.css"; +import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { addButton, removeButton } from "@api/MessagePopover"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { ChannelStore, Menu } from "@webpack/common"; @@ -55,25 +55,16 @@ export default definePlugin({ name: "Translate", description: "Translate messages with Google Translate", authors: [Devs.Ven], - dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI"], + dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], settings, // not used, just here in case some other plugin wants it or w/e translate, - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], - start() { addAccessory("vc-translation", props => ); addContextMenuPatch("message", messageCtxPatch); + addChatBarButton("vc-translate", TranslateChatBarIcon); addButton("vc-translate", message => { if (!message.content) return null; @@ -101,13 +92,8 @@ export default definePlugin({ stop() { removePreSendListener(this.preSend); removeContextMenuPatch("message", messageCtxPatch); + removeChatBarButton("vc-translate"); removeButton("vc-translate"); removeAccessory("vc-translation"); }, - - chatBarIcon: (slateProps: any) => ( - - - - ) }); From 2c198e547ce2424ada0c8e2c067b2d2c7b435c7a Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 6 Feb 2024 16:50:21 +0100 Subject: [PATCH 41/50] Fix PreviewMessage icon being offcentre --- src/api/ChatButton.css | 4 ++++ src/api/ChatButtons.tsx | 8 +++++++- src/plugins/invisibleChat.desktop/index.tsx | 6 +++--- src/plugins/previewMessage/index.tsx | 10 ++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 src/api/ChatButton.css diff --git a/src/api/ChatButton.css b/src/api/ChatButton.css new file mode 100644 index 000000000..30869a846 --- /dev/null +++ b/src/api/ChatButton.css @@ -0,0 +1,4 @@ +.vc-chatbar-button { + display: flex; + align-items: center; +} diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx index 0350965af..c995033b1 100644 --- a/src/api/ChatButtons.tsx +++ b/src/api/ChatButtons.tsx @@ -4,12 +4,18 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import "./ChatButton.css"; + import ErrorBoundary from "@components/ErrorBoundary"; import { Logger } from "@utils/Logger"; +import { waitFor } from "@webpack"; import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; import { Channel } from "discord-types/general"; import { HTMLProps, MouseEventHandler, ReactNode } from "react"; +let CssClasses: { buttonContainer: string; }; +waitFor(["buttonContainer", "channelTextArea"], m => CssClasses = m); + export interface ChatBarProps { channel: Channel; disabled: boolean; @@ -100,7 +106,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { return ( {({ onMouseEnter, onMouseLeave }) => ( -
+