From 05b1fe8b16f17442af3c28b844c6177aabe29dc9 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 13 Mar 2026 15:00:09 +0100 Subject: [PATCH 1/4] fix PlatformIndicator badge --- src/plugins/_api/badges/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index 8d764a2b4..4229ee022 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -95,7 +95,7 @@ export default definePlugin({ replace: "...$1.props,$&" }, { - match: /(?<=forceOpen:.{0,40}?\i\((\i)\.id\).{0,100}?)children:/, + match: /(?<=forceOpen:.{0,40}?ariaHidden:!0,)children:(?=.{0,50}?(\i)\.id)/, replace: "children:$1.component?$self.renderBadgeComponent({...$1}) :" }, // handle onClick and onContextMenu From be8d7a4de1556f274ec9772e019e094be6f6a72c Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sun, 15 Mar 2026 01:04:07 +0100 Subject: [PATCH 2/4] CustomCommands: fix replying with commands --- .../src/stores/PendingReplyStore.d.ts | 15 +++++++++++++++ packages/discord-types/src/stores/index.d.ts | 1 + src/plugins/customCommands/index.ts | 6 ++++-- src/plugins/spotifyShareCommands/index.ts | 3 +-- src/plugins/voiceMessages/index.tsx | 5 ++--- src/webpack/common/stores.ts | 2 ++ 6 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 packages/discord-types/src/stores/PendingReplyStore.d.ts diff --git a/packages/discord-types/src/stores/PendingReplyStore.d.ts b/packages/discord-types/src/stores/PendingReplyStore.d.ts new file mode 100644 index 000000000..687601f10 --- /dev/null +++ b/packages/discord-types/src/stores/PendingReplyStore.d.ts @@ -0,0 +1,15 @@ +import { Channel, Message } from "../common"; +import { FluxStore } from "./FluxStore"; + +export interface PendingReply { + channel: Channel; + message: Message; + shouldMention: boolean; + showMentionToggle: boolean; +} + +export class PendingReplyStore extends FluxStore { + getPendingReply(channelId: string): PendingReply | undefined; + /** Discord doesn't use this method. Also seems to always return undefined */ + getPendingReplyActionSource(channelId: string): unknown; +} diff --git a/packages/discord-types/src/stores/index.d.ts b/packages/discord-types/src/stores/index.d.ts index 12f70760c..366467f1a 100644 --- a/packages/discord-types/src/stores/index.d.ts +++ b/packages/discord-types/src/stores/index.d.ts @@ -23,6 +23,7 @@ export * from "./MediaEngineStore"; export * from "./MessageStore"; export * from "./NotificationSettingsStore"; export * from "./OverridePremiumTypeStore"; +export * from "./PendingReplyStore"; export * from "./PermissionStore"; export * from "./PopoutWindowStore"; export * from "./PresenceStore"; diff --git a/src/plugins/customCommands/index.ts b/src/plugins/customCommands/index.ts index e9d857bd5..37edf902a 100644 --- a/src/plugins/customCommands/index.ts +++ b/src/plugins/customCommands/index.ts @@ -23,6 +23,7 @@ import { migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { sendMessage } from "@utils/discord"; import definePlugin from "@utils/types"; +import { FluxDispatcher, MessageActions, PendingReplyStore } from "@webpack/common"; import { openCreateTagModal } from "./CreateTagModal"; import { getTag, getTags, removeTag, settings, Tag } from "./settings"; @@ -67,7 +68,7 @@ export function registerTagCommand(tag: Tag) { } ], - execute: async (args, ctx) => { + execute: async (args, { channel }) => { const ephemeral = findOption(args, "ephemeral", false); const response = tag.message @@ -78,7 +79,8 @@ export function registerTagCommand(tag: Tag) { .replaceAll("\\n", "\n"); const doSend = ephemeral ? sendBotMessage : sendMessage; - doSend(ctx.channel.id, { content: response }); + doSend(channel.id, { content: response }, false, MessageActions.getSendMessageOptionsForReply(PendingReplyStore.getPendingReply(channel.id))); + FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId: channel.id }); }, [CustomCommandsMarker]: true, }, "CustomCommands"); diff --git a/src/plugins/spotifyShareCommands/index.ts b/src/plugins/spotifyShareCommands/index.ts index d00d4a2ca..b4a85eb16 100644 --- a/src/plugins/spotifyShareCommands/index.ts +++ b/src/plugins/spotifyShareCommands/index.ts @@ -22,7 +22,7 @@ import { sendMessage } from "@utils/discord"; import definePlugin from "@utils/types"; import { Command } from "@vencord/discord-types"; import { findByPropsLazy } from "@webpack"; -import { FluxDispatcher, MessageActions } from "@webpack/common"; +import { FluxDispatcher, MessageActions, PendingReplyStore } from "@webpack/common"; interface Album { id: string; @@ -55,7 +55,6 @@ interface Track { } const Spotify = findByPropsLazy("getPlayerState"); -const PendingReplyStore = findByPropsLazy("getPendingReply"); function makeCommand(name: string, formatUrl: (track: Track) => string): Command { return { diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index 6452f5ffb..4d1d54602 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -32,8 +32,8 @@ import definePlugin from "@utils/types"; import { chooseFile } from "@utils/web"; import { CloudUpload as TCloudUpload } from "@vencord/discord-types"; import { CloudUploadPlatform } from "@vencord/discord-types/enums"; -import { findLazy, findStoreLazy } from "@webpack"; -import { Button, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; +import { findLazy } from "@webpack"; +import { Button, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; import { ComponentType } from "react"; import { VoiceRecorderDesktop } from "./DesktopRecorder"; @@ -42,7 +42,6 @@ import { VoicePreview } from "./VoicePreview"; import { VoiceRecorderWeb } from "./WebRecorder"; const CloudUpload: typeof TCloudUpload = findLazy(m => m.prototype?.trackUploadFinished); -const PendingReplyStore = findStoreLazy("PendingReplyStore"); export const cl = classNameFactory("vc-vmsg-"); export type VoiceRecorder = ComponentType<{ diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 0d43e6356..31d552f8a 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -36,6 +36,7 @@ export let GuildChannelStore: t.GuildChannelStore; export let ReadStateStore: t.ReadStateStore; export let PresenceStore: t.PresenceStore; export let AccessibilityStore: t.AccessibilityStore; +export let PendingReplyStore: t.PendingReplyStore; export let GuildStore: t.GuildStore; export let GuildRoleStore: t.GuildRoleStore; @@ -130,6 +131,7 @@ waitForStore("LocaleStore", m => LocaleStore = m); waitForStore("RTCConnectionStore", m => RTCConnectionStore = m); waitForStore("SoundboardStore", m => SoundboardStore = m); waitForStore("PopoutWindowStore", m => PopoutWindowStore = m); +waitForStore("PendingReplyStore", m => PendingReplyStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; // Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies. From b67bb009fcf6c47be975428f3dbe5befbce0fb73 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sun, 15 Mar 2026 02:00:53 +0100 Subject: [PATCH 3/4] ReviewDB: make profile review button like game collection button --- .../reviewDB/components/ReviewModal.tsx | 4 +- .../reviewDB/components/ReviewsView.tsx | 4 +- src/plugins/reviewDB/index.tsx | 77 ++++++++++++++----- src/plugins/reviewDB/reviewDbApi.ts | 19 ++--- 4 files changed, 71 insertions(+), 33 deletions(-) diff --git a/src/plugins/reviewDB/components/ReviewModal.tsx b/src/plugins/reviewDB/components/ReviewModal.tsx index 9cc1e577d..c4da56642 100644 --- a/src/plugins/reviewDB/components/ReviewModal.tsx +++ b/src/plugins/reviewDB/components/ReviewModal.tsx @@ -19,7 +19,7 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Auth } from "@plugins/reviewDB/auth"; import { ReviewType } from "@plugins/reviewDB/entities"; -import { Response, REVIEWS_PER_PAGE } from "@plugins/reviewDB/reviewDbApi"; +import { REVIEWS_PER_PAGE, UserReviewsData } from "@plugins/reviewDB/reviewDbApi"; import { cl } from "@plugins/reviewDB/utils"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useForceUpdater } from "@utils/react"; @@ -29,7 +29,7 @@ import ReviewComponent from "./ReviewComponent"; import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; function Modal({ modalProps, modalKey, discordId, name, type }: { modalProps: any; modalKey: string, discordId: string; name: string; type: ReviewType; }) { - const [data, setData] = useState(); + const [data, setData] = useState(); const [signal, refetch] = useForceUpdater(true); const [page, setPage] = useState(1); diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index 675bfe614..558db9621 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -18,7 +18,7 @@ import { Auth, authorize } from "@plugins/reviewDB/auth"; import { Review, ReviewType } from "@plugins/reviewDB/entities"; -import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "@plugins/reviewDB/reviewDbApi"; +import { addReview, getReviews, REVIEWS_PER_PAGE, UserReviewsData } from "@plugins/reviewDB/reviewDbApi"; import { settings } from "@plugins/reviewDB/settings"; import { cl, showToast } from "@plugins/reviewDB/utils"; import { useAwaiter, useForceUpdater } from "@utils/react"; @@ -39,7 +39,7 @@ interface UserProps { } interface Props extends UserProps { - onFetchReviews(data: Response): void; + onFetchReviews(data: UserReviewsData): void; refetchSignal?: unknown; showInput?: boolean; page?: number; diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index 1b395f69c..51e0a3f3e 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -20,9 +20,12 @@ import "./style.css"; import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import ErrorBoundary from "@components/ErrorBoundary"; -import { NotesIcon, OpenExternalIcon } from "@components/Icons"; -import { TooltipContainer } from "@components/TooltipContainer"; +import { OpenExternalIcon } from "@components/Icons"; +import { Paragraph } from "@components/Paragraph"; +import { Span } from "@components/Span"; import { Devs } from "@utils/constants"; +import { classes } from "@utils/misc"; +import { useAwaiter } from "@utils/react"; import definePlugin from "@utils/types"; import { Guild, User } from "@vencord/discord-types"; import { findCssClassesLazy } from "@webpack"; @@ -31,11 +34,14 @@ import { Alerts, Clickable, Menu, Parser } from "@webpack/common"; import { Auth, initAuth, updateAuth } from "./auth"; import { openReviewsModal } from "./components/ReviewModal"; import { NotificationType, ReviewType } from "./entities"; -import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; +import { getCurrentUserInfo, getReviews, readNotification } from "./reviewDbApi"; import { settings } from "./settings"; import { showToast } from "./utils"; -const BannerButtonClasses = findCssClassesLazy("bannerButton"); +const DMSideBarClasses = findCssClassesLazy("widgetPreviews"); +const ProfileCardClasses = findCssClassesLazy("cardsList", "firstCardContainer", "card", "container"); +const ProfileCardContainerClasses = findCssClassesLazy("innerContainer", "icons", "icon", "displayCount", "displayCountText", "displayCountTextColor", "breadcrumb"); +const ProfileCardOverlayClasses = findCssClassesLazy("overlay", "isPrivate", "outer"); const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => { if (!guild) return; @@ -77,17 +83,19 @@ export default definePlugin({ patches: [ { - find: ".POPOUT,user:", + // DM profile sidebar + find: ".SIDEBAR,disableToolbar:", replacement: { - match: /children:\[(?=[^[]+?shouldShowTooltip:)/, - replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user})," + match: /user:(\i),widgets:.{0,100}?\}\),/, + replace: "$&$self.renderProfileComponent({user:$1,isSideBar:true})," } }, { - find: ".SIDEBAR,disableToolbar:", + // User popout + find: /\.POPOUT,onClose:\i}\),nicknameIcons:.+?\.isProvisional/, replacement: { - match: /children:\[(?=[^[]+?\.SIDEBAR}\),\i\.bot)/, - replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user})," + match: /user:(\i),widgets:.{0,100}?\}\),/, + replace: "$&$self.renderProfileComponent({user:$1})," } } ], @@ -146,16 +154,45 @@ export default definePlugin({ }, 4000); }, - BiteSizeReviewsButton: ErrorBoundary.wrap(({ user }: { user: User; }) => { - return ( - - openReviewsModal(user.id, user.username, ReviewType.User)} - className={BannerButtonClasses.bannerButton} - > - - - + renderProfileComponent: ErrorBoundary.wrap(({ user, isSideBar = false }: { user: User; isSideBar?: boolean; }) => { + const [reviewData] = useAwaiter(() => getReviews(user.id), { deps: [user.id], fallbackValue: null }); + + // Discord are masters at using a crap ton of html elements and css classes to create a simple ui that could have + // been made with less than half of the number of elements, so we have to do this insanity to replicate their ui + const reviewsSection = ( +
+
    +
  • + openReviewsModal(user.id, user.username, ReviewType.User)}> +
    + User Reviews + {!!reviewData?.reviewCount && ( +
    + {reviewData.reviews.slice(0, 4).map((review, idx) => { + const showCount = idx === 3 && reviewData.reviewCount > 4; + + return ( +
    + {review.sender.username} + {showCount && ( +
    + +{reviewData.reviewCount - 3} +
    + )} +
    + ); + })} +
    + )} +
    +
    +
  • +
+
); + + return isSideBar + ?
{reviewsSection}
+ : reviewsSection; }, { noop: true }) }); diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts index 3fe6dd061..e899216a3 100644 --- a/src/plugins/reviewDB/reviewDbApi.ts +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -27,7 +27,7 @@ const API_URL = "https://manti.vendicated.dev/api/reviewdb"; export const REVIEWS_PER_PAGE = 50; -export interface Response { +export interface UserReviewsData { message: string; reviews: Review[]; updated: boolean; @@ -47,18 +47,19 @@ async function rdbRequest(path: string, options: RequestInit = {}) { }); } -export async function getReviews(id: string, offset = 0): Promise { +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) + offset: String(offset), + limit: "4" }); const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`); const res = (req.ok) - ? await req.json() as Response + ? await req.json() as UserReviewsData : { 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: [], @@ -93,7 +94,7 @@ export async function getReviews(id: string, offset = 0): Promise { return res; } -export async function addReview(review: any): Promise { +export async function addReview(review: any): Promise { const token = await getToken(); if (!token) { @@ -109,13 +110,13 @@ export async function addReview(review: any): Promise { "Content-Type": "application/json", } }).then(async r => { - const data = await r.json() as Response; + const data = await r.json() as UserReviewsData; showToast(data.message); return r.ok ? data : null; }); } -export async function deleteReview(id: number): Promise { +export async function deleteReview(id: number): Promise { return await rdbRequest(`/users/${id}/reviews`, { method: "DELETE", headers: { @@ -126,7 +127,7 @@ export async function deleteReview(id: number): Promise { reviewid: id }) }).then(async r => { - const data = await r.json() as Response; + const data = await r.json() as UserReviewsData; showToast(data.message); return r.ok ? data : null; }); @@ -142,7 +143,7 @@ export async function reportReview(id: number) { body: JSON.stringify({ reviewid: id, }) - }).then(r => r.json()) as Response; + }).then(r => r.json()) as UserReviewsData; showToast(res.message); } From d177f4d597cbc3619f0e8b074ed070565273b146 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sun, 15 Mar 2026 02:23:53 +0100 Subject: [PATCH 4/4] AnonymiseFilenames: use hex alphabet to remove chance to generate bad words --- src/plugins/anonymiseFileNames/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index 985fc4a82..9b7690f34 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -53,7 +53,7 @@ const settings = definePluginSettings({ randomisedLength: { description: "Random characters length", type: OptionType.NUMBER, - default: 7, + default: 10, disabled: () => settings.store.method !== Methods.Random, }, consistent: { @@ -123,7 +123,7 @@ export default definePlugin({ const newFilename = (() => { switch (settings.store.method) { case Methods.Random: - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const chars = "ABCDEFabcdef0123456789"; return Array.from( { length: settings.store.randomisedLength }, () => chars[Math.floor(Math.random() * chars.length)]