From d6ede6fa3c934069ca152d0b3ef6cacfcc15a948 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:50:35 -0500 Subject: [PATCH 1/6] ghosted: fix error --- src/equicordplugins/ghosted/GhostedUsersModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/equicordplugins/ghosted/GhostedUsersModal.tsx b/src/equicordplugins/ghosted/GhostedUsersModal.tsx index fec53d66d..eb85658b5 100644 --- a/src/equicordplugins/ghosted/GhostedUsersModal.tsx +++ b/src/equicordplugins/ghosted/GhostedUsersModal.tsx @@ -30,6 +30,7 @@ function GroupDmsIcon({ channel }: { channel: Channel; }) { aria-label={channel?.name || "Unnamed Group"} /> : ; From 484cf5d67ba8f634fb8fb20b9bc2152c4ef2e6f7 Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:47:07 +0000 Subject: [PATCH 2/6] feat(plugin): Dragify (#684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * first and last commit * fixed. * reuse invite setting toggle * complete rewrite * sushi slide * woah new setting alert * polish * Optimizations + fix merge conflict lol * more optimizations * my code editor did NOT tell me about this error * t * Update index.tsx * fixes * fixes * patch fixes * Dragify's finally done * drop accidental changes * patch fixes * feat(MessageClickActions): add REPLY to own messages (#677) - also adds a click timeout to double click (for MS) * Apply suggestion from @Vendicated * improve ExpandableList find * make plain folder icon not cursed + fix incorrect patch it was missing the ! before we insert our class * limit ml patch length * boom 5 * Apply suggestion from @sadan4 Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> * fix the boom I broke * Apply suggestion from @Vendicated * limit length * Apply suggestion from @Vendicated * fix find for friends since * limit length * fix lint * so good * limit length * fix plainfoldericon * improve IgnoreActitivies * Update src/plugins/messageLogger/index.tsx Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> * Update src/plugins/messageLogger/index.tsx Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> * I'm so smart * limit length * stupid x2 * fix IgnoreActivities * limit length * fix * fix showHiddenThings * sssss * make backref lazy * fix: vcNarratorCustom user leave voice event filtering bug (#671) * fix(vcNarratorCustom): voice state event filtering is stricter now and has a guard against false positive userleave events * refactor(vcNarratorCustom): remove unused audio toggle functions for self-mute and self-deaf, they're not wired into the menu anyways. I forgot about them cause i just use discords built in mute/deafen sound for my own audio * fix(vcNarratorCustom): ensure voice state snapshot updates only occur if myChanId is defined as an extra guard * migrate to new switch style (#669) * fix everything (#3925) Co-authored-by: prism <69634294+imjustprism@users.noreply.github.com> Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Co-authored-by: V Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: prism Co-authored-by: wearrrrr Co-authored-by: Elvyra <88881326+EepyElvyra@users.noreply.github.com> * bump to v1.14.1 * WebScreenshareFixes: remove codec changes This causes too many issues unfortunately * ExpressionCloner: fix cloning animated emojis * Maybe bug fix. (#678) * Add support for silent pings in notifications (#679) * fix * Decor: Fix change decoration modal being unable to be opened * Questify Bug Fix (Real) (#680) * Maybe bug fix. * Actually fixed this time (real). * Remove log. * PlatformSpoofer: Do a terrible patch * RoleColorEverywhere: Fix not working on voice channel and chat (#3934) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> * bunp discord-types * apply patch fix to rolecoloreverywhere * remove duplicate EquicordDevs.Prism * fix(mediaPlaybackSpeed): update color vars, fix alignment (#681) * fix(mediaPlaybackSpeed): update color vars to fix on voice message * fix(mediaPlaybackSpeed): fix alignment * fix prism * MessageClickActions: Click actions no longer trigger on message links (#683) * fix(MessageClickActions): stuff fixes click actions triggering on message links, also use the double click timeout for single click * fix(MessageClickActions): only delay actions if current action is valid * fix(MessageClickActions): proactive modifier checking * add fileUpload plugin (#685) * add fileUpload plugin * nest * types * css * hey man * nest -> destkop only, per service tokens, hide only service per options * Update upload.ts --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> * feat(FileUpload): add E-Z to hosts (#687) * Questify: Feature and bugfix. (#689) * Feature and bugfix. * Improve comment. * cool changes (#682) * BetterUserArea: fix some stuff * bleh * add better update notification * wau * HeaderBarApi: fix new style * Fix clips (kinda) (#688) * add ignorePlatformRestriction toggle * Clean up types * fix intl --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> * update css * chore: KeywordNotify cleanup (#692) * remove commas from highlights in inbox * use Button Component for Clear All button * fix other buttons * cleanup try catches * keywordnotify update * feat: prevent middle-click link open (option) (#690) * feat: add NoMiddleOpen plugin * feat: extend LimitMiddleClickPaste * ReviewDB: Fix viewing reviews with new CSS mangling * NoBlockedMessages: Fix extra space between messages * WebContextMenus: Fix restoring Copy & Save Image * Fix Most Broken Patches * Add FavoriteAnything * LimitMiddleClickPaste: Description * fix patch * Fix patches for latest Discord update (#3943) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> * fix(SupportHelper): make update button check case-insensitive (#3939) * revert toastnotif edits * update and add components (#691) * update: switch * add: notice * update: link * add: badge * update heading * update: basetext * update: card * fix: heading * components-dev: fix * lint * update: margins * KeywordNotify: Revert some changes * Make Messageclickactions Great Again! (#695) forgive me father for i have sinned, and thank you prism for fixing my awful code * add equicord tray menu for desktop discord (#694) * fix pnpm inject on local builds (#696) * bypass cors to fix e-z uploads (#697) * channelTabs macos tweaks (#701) * fix: show info icon instead of cog for plugins with only hidden settings (#699) * Questify: Improve panel override patch and add ctx option to resume Auto Complete. (#698) * Improve panel override, add ctx option. * Improve panel text. * holyNotes: fix - Modal Fixes, Don't Update Global UserStore (#693) * Fix: holyNotes - Make Modal Actually Bearable * Fix: holyNotes - Fix Double Declaration * Fix: holyNotes - Don't Change Global UserStore Co-Authored-By: prism <69634294+imjustprism@users.noreply.github.com> * QOL: holyNotes - Change Bot Tag to "NOTE" * Fix: holyNotes - Fuck Your Comments GitHub Copilot, I Thought I Turned You Off * Feat: holyNotes - UI Overhaul, Fixed Spaghetti CSS * Fix: holyNotes - Fix CSS Classes * Fix: holyNotes - Fix Fetching User Profiles for Notes --------- Co-authored-by: prism <69634294+imjustprism@users.noreply.github.com> * This better not break again for at least a year. (#702) * Questify: CSS Fix. (#703) * This better not break again for at least a year. * CSS. * fix VoiceMessages plugin * Settings: add textarea options * misc Settings ui improvements * VcNarrator: only narrate events of current session * CrashHandler: log component stack on crash (#3933) * Translate: update DeepL language list (#3919) * StartupTimings: Fix duplicate find * CopyFileContents: Fix broken patch * ChatInputButtonAPI: Fix broken patch * ShowConnections: Fix broken patch * Fix Patches * remove styles * Questify: 2025-12-quest-cta-refactor-rollout support. (#704) * 2025-12-quest-cta-refactor-rollout support. * Fix. * BetterActivities: Update * BetterActivities: Remove logs * MessageClickActions: Update (#706) * ThemeLibrary: Fix Key * ThemeLibrary: Fix Key * LoginWithQR: Fixes * fixes (#705) * HeaderBarApi: fix minor style issue * HolyNotes: replace icon * Components.dev: remove popout * HeaderBarApi: fix tooltip * HolyNotes: replace icon again * HolyNotes: refactor * Switch: fix thumb color for whtie theme * BetterUserArea: fix patch * ModalCloseButton: remove dups and export properly * bleh * Update NotebookCreateModal.tsx * bleh --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> * BetterActivities: Fix Spacing * KeywordNotify: Update * bump * Equicord Tray: Update Repair * Fix Dummy Filled Authors Co-Authored-By: Etorix <92535668+etorixdev@users.noreply.github.com> * DevCompanion: Update * Tray: Rename IPC Events * Tray: About page * About: Redesign to match Equicord website (#709) * Tray: Hyperlink Version & Commit * Message click actions (yes, again) (#710) * fix(MessageClickActions): stop sticky modifiers caused by not cleaning up * fix(MessageClickActions): double clicks now double click double as consistently * she make on my range till i marker * chore: patch improvements * chore: fix remaining merge conflicts and resolve duplicates * chore: sync with upstream dev and remove broken vencondToolbox * fixes * greatest commit of all time * patch improvements and code quality improvements * fucking hell * bleh * Update index.tsx * hopefully the last commit * code quality improvements * can this shit end * lint * error misfire fix * lint * cook dragify back * Update index.tsx * LET IT END --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: Key Co-authored-by: V Co-authored-by: sadan <117494111+sadan4@users.noreply.github.com> Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Co-authored-by: Sam <82487541+example-git@users.noreply.github.com> Co-authored-by: prism Co-authored-by: prism <69634294+imjustprism@users.noreply.github.com> Co-authored-by: wearrrrr Co-authored-by: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Co-authored-by: Etorix <92535668+EtorixDev@users.noreply.github.com> Co-authored-by: DjJewl <47831173+wolflord212312@users.noreply.github.com> Co-authored-by: verticalsync <60797172+verticalsync@users.noreply.github.com> Co-authored-by: Nico Co-authored-by: creations Co-authored-by: x3rt Co-authored-by: korzi <106588537+notkorzi@users.noreply.github.com> Co-authored-by: Dog <104234930+dgxo@users.noreply.github.com> Co-authored-by: Z ❤'s Life Co-authored-by: Vũ Minh Nguyên --- src/equicordplugins/bypassStatus/index.tsx | 4 +- src/equicordplugins/dragify/ghost.tsx | 237 ++++ src/equicordplugins/dragify/index.tsx | 1225 ++++++++++++++++++++ src/equicordplugins/dragify/style.css | 149 +++ src/equicordplugins/dragify/utils.ts | 182 +++ src/equicordplugins/ingtoninator/index.tsx | 4 +- 6 files changed, 1797 insertions(+), 4 deletions(-) create mode 100644 src/equicordplugins/dragify/ghost.tsx create mode 100644 src/equicordplugins/dragify/index.tsx create mode 100644 src/equicordplugins/dragify/style.css create mode 100644 src/equicordplugins/dragify/utils.ts diff --git a/src/equicordplugins/bypassStatus/index.tsx b/src/equicordplugins/bypassStatus/index.tsx index 7f86538b5..ac9d635d0 100644 --- a/src/equicordplugins/bypassStatus/index.tsx +++ b/src/equicordplugins/bypassStatus/index.tsx @@ -22,7 +22,7 @@ interface IMessageCreate { message: Message; } -const SILENT_PING_FLAG = 1 << 12; +const SILENT_PING_FLAG = 1 << 12; function Icon(enabled?: boolean): JSX.Element { return ; + +const cl = classNameFactory("vc-dragify-"); + +let ghostRoot: ReturnType | null = null; +let ghostMountNode: HTMLDivElement | null = null; +let ghostRaf: number | null = null; +let ghostPendingPos: { x: number; y: number; } | null = null; +let ghostHideTimer: number | null = null; + +let ghostState: GhostState = { + visible: false, + x: 0, + y: 0, + kind: "channel", + title: "", + exiting: false +}; + +const ghostListeners = new Set<() => void>(); + +function notifyGhost() { + ghostListeners.forEach(listener => listener()); +} + +function setGhostState(next: Partial) { + ghostState = { ...ghostState, ...next }; + notifyGhost(); +} + +export function isGhostVisible(): boolean { + return ghostState.visible; +} + +export function scheduleGhostPosition(x: number, y: number) { + ghostPendingPos = { x, y }; + if (ghostRaf !== null) return; + setGhostState({ x, y }); + ghostRaf = requestAnimationFrame(() => { + if (ghostPendingPos) setGhostState({ x: ghostPendingPos.x, y: ghostPendingPos.y }); + ghostPendingPos = null; + ghostRaf = null; + }); +} + +export function hideGhost() { + if (!ghostState.visible) return; + if (ghostHideTimer !== null) { + clearTimeout(ghostHideTimer); + ghostHideTimer = null; + } + setGhostState({ exiting: true }); + ghostHideTimer = window.setTimeout(() => { + ghostHideTimer = null; + setGhostState({ visible: false, exiting: false }); + }, 200); +} + +export function showGhost(next: Omit, position?: { x: number; y: number; }) { + if (ghostHideTimer !== null) { + clearTimeout(ghostHideTimer); + ghostHideTimer = null; + } + if (position) scheduleGhostPosition(position.x, position.y); + setGhostState({ ...next, visible: true, exiting: false }); +} + +export function mountGhost() { + if (typeof document === "undefined") return; + const { body } = document; + if (!body) return; + + if (ghostRoot) { + if (ghostMountNode && !ghostMountNode.isConnected) body.appendChild(ghostMountNode); + return; + } + + if (!ghostMountNode) ghostMountNode = document.createElement("div"); + if (!ghostMountNode.isConnected) body.appendChild(ghostMountNode); + + ghostRoot = createRoot(ghostMountNode); + ghostRoot.render( + + + + + + ); +} + +export function unmountGhost() { + ghostRoot?.unmount(); + ghostRoot = null; + + ghostMountNode?.remove(); + ghostMountNode = null; + + if (ghostRaf !== null) { + cancelAnimationFrame(ghostRaf); + ghostRaf = null; + } + ghostPendingPos = null; + + if (ghostHideTimer !== null) { + clearTimeout(ghostHideTimer); + ghostHideTimer = null; + } + setGhostState({ visible: false, exiting: false }); +} + +const DragGhost = () => { + const [state, setState] = useState(ghostState); + useEffect(() => { + const listener = () => setState({ ...ghostState }); + ghostListeners.add(listener); + return () => { + ghostListeners.delete(listener); + }; + }, []); + + const voiceState = useStateFromStores( + [VoiceStateStore], + () => (state.kind === "user" && state.entityId + ? VoiceStateStore.getVoiceStateForUser(state.entityId) + : null) + ); + const inVoice = voiceState?.channelId; + const isMuted = voiceState && (voiceState.selfMute || voiceState.mute || voiceState.selfDeaf || voiceState.deaf); + const isStreaming = voiceState?.selfStream; + + if (!state.visible) return null; + + return ( + + + + {state.iconUrl + ? + : {state.symbol ?? "#"} + } + + + + {state.title} + {state.kind === "user" && inVoice + ? (isMuted + ? + : ) + : null} + {state.kind === "user" && inVoice && isStreaming + ? + : null} + + {state.subtitle && {state.subtitle}} + + {state.badge ?? state.kind} + + + ); +}; + +function VoiceStateIcon({ className, size = 14 }: { className?: string; size?: number; }) { + return ( + + + + ); +} + +function VoiceMutedIcon({ className, size = 14 }: { className?: string; size?: number; }) { + return ( + + + + ); +} diff --git a/src/equicordplugins/dragify/index.tsx b/src/equicordplugins/dragify/index.tsx new file mode 100644 index 000000000..32058d694 --- /dev/null +++ b/src/equicordplugins/dragify/index.tsx @@ -0,0 +1,1225 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./style.css"; + +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import { getGuildAcronym, insertTextIntoChatInputBox } from "@utils/discord"; +import { Logger } from "@utils/Logger"; +import definePlugin, { OptionType } from "@utils/types"; +import type { Channel } from "@vencord/discord-types"; +import { ChannelType } from "@vencord/discord-types/enums"; +import { ChannelStore, DraftActions, DraftStore, DraftType, GuildChannelStore, GuildStore, IconUtils, PermissionsBits, PermissionStore, RelationshipStore, RestAPI, SelectedChannelStore, showToast, Toasts, UserStore } from "@webpack/common"; + +import { type GhostState, hideGhost as hideDragGhost, isGhostVisible, mountGhost as mountDragGhost, scheduleGhostPosition as scheduleDragGhostPosition, showGhost as showDragGhost, unmountGhost as unmountDragGhost } from "./ghost"; +import { collectPayloadStrings, type DropEntity, extractChannelFromUrl, extractChannelPath, extractSnowflakeFromString, extractUserFromAvatar, extractUserFromProfile, parseFromStrings, tryParseJson } from "./utils"; + +const logger = new Logger("Dragify"); +type DragifyRuntime = { + onDrop(event: DragEvent, channel?: Channel | null): Promise; + isMessageInputEvent(event: DragEvent): boolean; + isAttachmentElement(target: Element | null): boolean; + isMessageInputElement(el: Element | null): boolean; + extractUserIdFromTarget(target: Element | null): string | null; + extractUserIdFromEvent(event: DragEvent): string | null; + onUserDragStart(event: DragEvent, user?: { id: string; userId?: string; user?: { id: string; }; }): void; + extractAvatarUserIdFromTarget(target: HTMLElement | null): string | null; + extractChannelIdFromTarget(target: HTMLElement | null): { id: string; guildId?: string; } | null; + onChannelDragStart(event: DragEvent, channel?: Pick | { id: string; guildId?: string; }): void; + extractGuildIdFromTarget(target: HTMLElement): string | null; + showGhost(entity: DropEntity, event?: DragEvent): void; +}; + +let pluginInstance: DragifyRuntime | null = null; +let activeGuildDragId: string | null = null; +let activeUserDragId: string | null = null; +let activeDragEntity: DropEntity | null = null; +let lastDropAt = 0; +let lastHandledDrop: { at: number; key: string; } = { at: 0, key: "" }; +let lastDragEventAt = 0; +let guildGhostCleanupTimer: number | null = null; +let dragifyActive = false; +let dragStateWatchdog: number | null = null; +let dragSourceIsInput = false; +const dropDedupeWindowMs = 150; +let transparentDragImage: HTMLCanvasElement | null = null; + +function getTransparentDragImage(): HTMLCanvasElement | null { + if (typeof document === "undefined") return null; + if (transparentDragImage) return transparentDragImage; + const canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + transparentDragImage = canvas; + return canvas; +} + +function suppressDefaultDragPreview(event: DragEvent) { + const image = getTransparentDragImage(); + if (!image || !event.dataTransfer?.setDragImage) return; + event.dataTransfer.setDragImage(image, 0, 0); +} + +function shouldIgnoreDrop(key: string): boolean { + const now = Date.now(); + if (lastHandledDrop.key === key && now - lastHandledDrop.at < dropDedupeWindowMs) return true; + lastHandledDrop = { key, at: now }; + return false; +} + +const settings = definePluginSettings({ + userOutput: { + type: OptionType.SELECT, + description: "User drop output.", + options: [ + { label: "Mention user", value: "mention", default: true }, + { label: "User ID", value: "id" }, + ], + }, + channelOutput: { + type: OptionType.SELECT, + description: "Channel drop output.", + options: [ + { label: "#channel mention", value: "mention", default: true }, + { label: "Channel link", value: "link" }, + ], + }, + inviteExpireAfter: { + type: OptionType.SELECT, + description: "Invite expiration.", + options: [ + { label: "30 minutes", value: 1800 }, + { label: "1 hour", value: 3600 }, + { label: "6 hours", value: 21600 }, + { label: "12 hours", value: 43200 }, + { label: "1 day", value: 86400 }, + { label: "7 days", value: 604800 }, + { label: "Never", value: 0, default: true }, + ], + }, + inviteMaxUses: { + type: OptionType.SELECT, + description: "Invite max uses.", + options: [ + { label: "No limit", value: 0, default: true }, + { label: "1 use", value: 1 }, + { label: "5 uses", value: 5 }, + { label: "10 uses", value: 10 }, + { label: "25 uses", value: 25 }, + { label: "50 uses", value: 50 }, + { label: "100 uses", value: 100 }, + ], + }, + inviteTemporaryMembership: { + type: OptionType.BOOLEAN, + default: false, + description: "Grant temporary membership.", + }, + reuseExistingInvites: { + type: OptionType.BOOLEAN, + default: false, + description: "Reuse existing invite instead of creating a new one.", + }, + allowChatBodyDrop: { + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true, + description: "Allow dropping into the main chat body to insert text.", + }, +}); + +function clearDragState() { + activeUserDragId = null; + activeGuildDragId = null; + activeDragEntity = null; + dragifyActive = false; + dragSourceIsInput = false; +} + +function setDragifyDataTransfer(dataTransfer: DataTransfer | null, payload: string) { + if (!dataTransfer) return; + if (dataTransfer.clearData) { + dataTransfer.clearData("text/plain"); + dataTransfer.clearData("text/uri-list"); + dataTransfer.clearData("text/html"); + } + dataTransfer.setData("application/json", payload); + dataTransfer.setData("application/dragify", payload); + dataTransfer.setData("text/plain", ""); +} + +function hasDragifyTransfer(dataTransfer?: DataTransfer | null) { + return dataTransfer?.types?.includes("application/dragify"); +} +const inviteCache = new Map(); + +export default definePlugin({ + name: "Dragify", + description: "Drop users, channels, or servers into chat to insert mentions or invites.", + authors: [EquicordDevs.justjxke], + settings, + + patches: [ + // Voice user rows (voice channel sidebar list) + { + find: "location:\"VoiceUser\"", + replacement: { + match: /(?<=previewIsOpen:.{0,100})"data-dnd-name":(\i)\.name,/, + replace: "$&\"data-dragify-user\":!0,\"data-user-id\":arguments[0].user?.id,draggable:!0,onDragStart:e=>$self.onUserDragStart(e,arguments[0].user)," + } + }, + { + find: "tutorialId:\"voice-conversations\"", + replacement: [ + // Voice channel rows (guild sidebar) + { + match: /(?<=this.getTooltipText\(\),.{0,100})iconClassName:/, + replace: "draggable:!0,onDragStart:e=>$self.onChannelDragStart(e,{id:this?.props?.channel?.id,guild_id:this?.props?.channel?.guild_id}),$&" + }, + // Voice channel rows (guild sidebar fallback - li wrapper) + { + match: /(?<=getModeClass\(\),.{0,50})"data-dnd-name":(\i)\.name,children:\[/, + replace: "draggable:!0,onDragStart:e=>$self.onChannelDragStart(e,{id:this?.props?.channel?.id,guild_id:this?.props?.channel?.guild_id}),$&" + } + ] + }, + // Voice channel rows (alternate voice list implementation) + { + find: "handleClickChat", + replacement: { + match: /(?<=getModeClass\(\),.{0,50})"data-dnd-name":(\i)\.name,/, + replace: "$&draggable:!0,onDragStart:t=>$self.onChannelDragStart(t,{id:this?.props?.channel?.id,guild_id:this?.props?.channel?.guild_id})," + } + }, + // Thread rows in channel list (sidebar thread items) + { + find: "__invalid_threadMainContent", + replacement: { + match: /className:\i\.\i,onClick:\i,"aria-label":(?=.{0,100}__invalid_threadMainContent)/, + replace: "draggable:!0,onDragStart:e=>$self.onChannelDragStart(e),$&" + } + }, + // Thread rows in channel list (threaded child rows) + // (handled by __invalid_threadMainContent patch) + // Channel list items (modern list wrapper) + { + find: "shouldShowThreadsPopout", + replacement: { + match: /(?<=getClassName\(\).{0,50})"data-dnd-name":(\i)\.name,/, + replace: "$&draggable:!0,onDragStart:e=>$self.onChannelDragStart(e,{id:this?.props?.channel?.id,guild_id:this?.props?.channel?.guild_id})," + } + }, + // Thread rows (active threads popout) + { + find: "POPOUT)},children:", + replacement: { + match: /(?<=getUser\(\i\.ownerId\).{0,100})className:\i\.\i,onClick:\i=>\{\(0,\i\.\i\)\((\i),/, + replace: "draggable:!0,onDragStart:e=>$self.onChannelDragStart(e,{id:arguments[0]?.thread?.id,guild_id:arguments[0]?.thread?.guild_id}),$&" + } + }, + // Member list items (server member list) + { + find: "MemberListItem", + replacement: { + match: /(?<=useRawTargetDimensions.{0,250})onContextMenu:\i,(?=onMouseEnter:)/, + replace: "$&draggable:!0,onDragStart:e=>$self.onUserDragStart(e,{id:arguments[0].user?.id}),\"data-dragify-user\":!0,\"data-user-id\":arguments[0].user?.id," + } + }, + // Chat usernames + { + find: ']="BADGES"', + replacement: { + match: /(?<="username"===.{0,100})(onClick:\i,onContextMenu:\i,)(?=children:)/, + replace: "$1draggable:!0,onDragStart:e=>$self.onUserDragStart(e,arguments[0].author),\"data-dragify-user\":!0,\"data-user-id\":arguments[0].author.id," + } + }, + // Call avatars (DM/group call tiles) + { + find: ".VOICE_CHANNEL_TILE,children:", + replacement: { + match: /(?<=\.VOICE_USER,.{0,250})className:\i\.\i,onDoubleClick:\i,/, + replace: "draggable:!0,onDragStart:e=>$self.onUserDragStart(e,{id:arguments[0].participantUserId}),\"data-dragify-user\":!0,\"data-user-id\":arguments[0].participantUserId,$&" + } + }, + // DM list entries (private channel rows) + { + find: "PrivateChannel.renderAvatar: Invalid prop configuration - no user or channel", + replacement: { + match: /\.CHANNEL\(\i\.\i,\i\.\i\),(?=.{0,150}\.isMultiUserDM\(\))/, + replace: "$&draggable:!0,onDragStart:e=>$self.onDmDragStart(e,arguments[0].channel)," + } + }, + { + find: "[aria-owns=folder-items-", + replacement: { + match: /"data-dnd-name":(\i)\.name,(?="data-drop-hovering".{0,100}formatToPlainString\()/, + replace: "$&draggable:!0,onDragStart:e=>$self.onGuildDragStart(e,$1.id)," + } + }, + ], + + async onDrop(event: DragEvent, channel?: Channel | null) { + if (dragSourceIsInput) return; + const { dataTransfer } = event; + if (!dataTransfer || dataTransfer.files?.length) return; + + const dragifyData = dataTransfer.getData("application/dragify"); + const hasActiveEntity = activeDragEntity || activeUserDragId || activeGuildDragId; + if (dragifyData || hasActiveEntity) { + lastDropAt = Date.now(); + event.preventDefault(); + event.stopPropagation(); + } + + const resolvedChannel = channel + ?? (SelectedChannelStore?.getChannelId?.() + ? ChannelStore.getChannel(SelectedChannelStore.getChannelId()) + : null); + if (!resolvedChannel) return; + + if (dragifyData) { + const parsed = tryParseJson<{ kind?: string; id?: string; guildId?: string; }>(dragifyData); + if (parsed?.kind && parsed.id) { + let fromDragify: DropEntity | null = null; + switch (parsed.kind) { + case "user": + fromDragify = { kind: "user", id: parsed.id }; + break; + case "channel": + fromDragify = { kind: "channel", id: parsed.id, guildId: parsed.guildId }; + break; + case "guild": + fromDragify = { kind: "guild", id: parsed.id }; + break; + } + if (fromDragify) { + const key = `${fromDragify.kind}:${fromDragify.id}:${resolvedChannel.id}`; + if (shouldIgnoreDrop(key)) return; + await this.handleDropEntity(fromDragify, resolvedChannel); + return; + } + } + } + + const payloads = await collectPayloadStrings(dataTransfer); + let entity = parseFromStrings(payloads, { ChannelStore, GuildStore, UserStore }); + if (!entity && activeDragEntity) { + entity = activeDragEntity; + } else if (!entity && activeUserDragId) { + entity = { kind: "user", id: activeUserDragId }; + } else if (!entity && activeGuildDragId) { + entity = { kind: "guild", id: activeGuildDragId }; + } + if (!entity) { + return; + } + + const key = `${entity.kind}:${entity.id}:${resolvedChannel.id}`; + if (shouldIgnoreDrop(key)) return; + await this.handleDropEntity(entity, resolvedChannel); + }, + + async handleDropEntity(entity: DropEntity, channel: Channel) { + try { + const text = await this.buildText(entity, channel); + if (!text) { + clearDragState(); + return; + } + let inserted = false; + if (entity.kind === "user") { + inserted = this.insertText(channel.id, text, { removeUnknownUser: true }); + } else { + inserted = this.insertText(channel.id, text); + } + if (!inserted) throw new Error("Unable to insert drag content"); + clearDragState(); + } catch (error) { + logger.error("Failed handling drop", error); + showToast("Dragify failed to handle drop.", Toasts.Type.FAILURE); + } + }, + + async buildText(entity: DropEntity, currentChannel: Channel): Promise { + switch (entity.kind) { + case "user": + return settings.store.userOutput === "id" ? entity.id : `<@${entity.id}>`; + case "channel": + return this.formatChannel(entity.id, entity.guildId); + case "guild": + return await this.createInvite(entity.id, currentChannel); + default: + return null; + } + }, + + formatChannel(channelId: string, guildId?: string): string | null { + const channel = ChannelStore.getChannel(channelId); + if (!channel) return null; + if (channel.isGroupDM() || channel.isMultiUserDM()) { + return settings.store.userOutput === "id" + ? channelId + : `https://discord.com/channels/@me/${channelId}`; + } + if (settings.store.channelOutput === "link") { + const effectiveGuildId = guildId ?? channel?.guild_id ?? "@me"; + return `https://discord.com/channels/${effectiveGuildId}/${channelId}`; + } + return `<#${channelId}>`; + }, + + async createInvite(guildId: string, currentChannel: Channel): Promise { + const inviteChannel = this.findInviteChannel(guildId, currentChannel); + const fallbackChannelId = inviteChannel ? null : await this.fetchInviteChannelId(guildId); + + if (settings.store.reuseExistingInvites) { + const cached = inviteCache.get(guildId); + if (cached && cached.maxUses === null && !this.isInviteExpired(cached)) return `https://discord.gg/${cached.code}`; + const reused = await this.fetchReusableInvite(guildId, inviteChannel ?? null); + if (reused) return `https://discord.gg/${reused}`; + } + + const inviteChannelId = inviteChannel?.id ?? fallbackChannelId; + if (!inviteChannelId) { + showToast("No channel available for invites.", Toasts.Type.FAILURE); + return null; + } + if (inviteChannel && inviteChannel.guild_id !== guildId) { + showToast("No channel available for invites.", Toasts.Type.FAILURE); + return null; + } + + try { + const maxAge = settings.store.inviteExpireAfter ?? 0; + const maxUses = settings.store.inviteMaxUses ?? 0; + const { body } = await RestAPI.post({ + url: `/channels/${inviteChannelId}/invites`, + body: { + max_age: maxAge, + max_uses: maxUses, + temporary: settings.store.inviteTemporaryMembership, + unique: true, + }, + }); + const code = typeof body === "object" && body ? (body as { code?: string; }).code : null; + if (!code) throw new Error("Invite response missing code"); + inviteCache.set(guildId, { + code, + expiresAt: maxAge > 0 ? Date.now() + maxAge * 1000 : null, + maxUses: maxUses === 0 ? null : maxUses, + uses: 0, + }); + showToast("Invite created.", Toasts.Type.SUCCESS); + return `https://discord.gg/${code}`; + } catch (error) { + logger.error("Failed to create invite", error); + showToast("Unable to create invite.", Toasts.Type.FAILURE); // uh oh! + return null; + } + }, + + async fetchInviteChannelId(guildId: string): Promise { + try { + const { body } = await RestAPI.get({ url: `/guilds/${guildId}/channels` }); + if (!Array.isArray(body)) return null; + + const candidates = body + .filter(ch => ch && ch?.id) + .filter(ch => { + const { type } = ch; + return type === ChannelType.GUILD_TEXT + || type === ChannelType.GUILD_ANNOUNCEMENT + || type === ChannelType.GUILD_FORUM + || type === ChannelType.GUILD_MEDIA; + }) + .sort((a, b) => (a.position ?? 0) - (b.position ?? 0)); + + return candidates[0]?.id ?? null; + } catch { + return null; + } + }, + + async fetchReusableInvite(guildId: string, inviteChannel: Channel | null): Promise { + const cached = inviteCache.get(guildId); + if (cached && cached.maxUses === null && !this.isInviteExpired(cached)) return cached.code; + + try { + const channelId = inviteChannel?.id ?? null; + const url = channelId ? `/channels/${channelId}/invites` : `/guilds/${guildId}/invites`; + + const { body } = await RestAPI.get({ url }); + if (!Array.isArray(body)) return null; + + const now = Date.now(); + const invite = body.find(inv => { + const expiresAt = inv.expires_at ? Date.parse(inv.expires_at) : null; + const maxUsesRaw = inv.max_uses ?? null; + const maxUses = maxUsesRaw === 0 ? null : maxUsesRaw; + const uses = inv.uses ?? null; + const notExpired = expiresAt === null || expiresAt > now; + const usesLeft = maxUses === null || uses === null || uses < maxUses; + return notExpired && usesLeft && typeof inv.code === "string"; + }); + + if (invite?.code) { + inviteCache.set(guildId, { + code: invite.code, + expiresAt: invite.expires_at ? Date.parse(invite.expires_at) : null, + maxUses: invite.max_uses === 0 ? null : invite.max_uses ?? null, + uses: invite.uses ?? null, + }); + return invite.code; + } + } catch { + // If we cannot list invites (permissions/403), fall back silently. + return null; + } + + return null; + }, + + isInviteExpired(invite: { expiresAt: number | null; maxUses: number | null; uses: number | null; }): boolean { + const now = Date.now(); + const expired = invite.expiresAt !== null && invite.expiresAt <= now; + const exhausted = invite.maxUses !== null && invite.uses !== null && invite.uses >= invite.maxUses; + return expired || exhausted; + }, + + findInviteChannel(guildId: string, currentChannel: Channel): Channel | null { + if (currentChannel.guild_id === guildId && this.canCreateInvite(currentChannel)) return currentChannel; + + const guild = GuildStore.getGuild(guildId); + const preferredIds = [ + guild?.systemChannelId, + guild?.rulesChannelId, + guild?.publicUpdatesChannelId, + ].filter(Boolean) as string[]; + for (const id of preferredIds) { + const channel = ChannelStore.getChannel(id); + if (this.canCreateInvite(channel)) return channel; + } + + const selectableStore = (GuildChannelStore.getSelectableChannels?.(guildId) ?? []) + .map(e => e.channel) + .filter(Boolean) as Channel[]; + const selectableCollection = (() => { + const collection = GuildChannelStore.getChannels(guildId); + if (!collection?.SELECTABLE) return []; + + const result: Channel[] = []; + for (const entry of Object.values(collection.SELECTABLE)) { + const channel = entry && entry?.channel ? entry.channel : entry; + if (channel) result.push(channel); + } + return result; + })(); + + const byId = new Map(); + for (const ch of [...selectableStore, ...selectableCollection]) { + if (!ch || ch.guild_id !== guildId) continue; + if (!byId.has(ch.id)) byId.set(ch.id, ch); + } + const candidates = [...byId.values()] + .sort((a, b) => { + const pa = typeof a.position === "number" ? a.position : 0; + const pb = typeof b.position === "number" ? b.position : 0; + if (pa === pb) return a.id.localeCompare(b.id); + return pa - pb; + }); + + for (const channel of candidates) { + if (this.canCreateInvite(channel)) return channel; + } + + return null; + }, + + canCreateInvite(channel?: Channel | null): channel is Channel { + if (!channel || !channel.guild_id) return false; + if (channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false; + if (channel.isCategory()) return false; + if (channel.isThread()) return false; + return PermissionStore.can(PermissionsBits.CREATE_INSTANT_INVITE, channel); + }, + + insertText(channelId: string, text: string, options?: { removeUnknownUser?: boolean; }): boolean { + let insertedIntoInput = false; + let insertedIntoDraft = false; + + try { + insertTextIntoChatInputBox(text); + insertedIntoInput = true; + } catch (error) { + logger.error("Failed to insert text into input", error); + } + + try { + let existing = DraftStore.getDraft(channelId, DraftType.ChannelMessage) ?? ""; + if (options?.removeUnknownUser) existing = existing.replace(/@unknown[- ]user/gi, "").trim(); + + const needsSpace = existing.length > 0 && !existing.endsWith(" "); + const nextValue = needsSpace ? `${existing} ${text}` : `${existing}${text}`; + + !existing ? DraftActions.saveDraft(channelId, nextValue, DraftType.ChannelMessage) : DraftActions.changeDraft(channelId, nextValue, DraftType.ChannelMessage); + insertedIntoDraft = true; + } catch (error) { + logger.error("Failed to update draft", error); + } + + return insertedIntoInput || insertedIntoDraft; + }, + + onChannelDragStart(event: DragEvent, channel?: Pick | { id: string; guildId?: string; }) { + if (activeUserDragId) return; + const existingDragify = event.dataTransfer?.getData("application/dragify") ?? ""; + if (existingDragify) { + const parsed = tryParseJson<{ kind?: string; }>(existingDragify); + if (parsed?.kind === "user") return; + } + const targetEl = event.target as HTMLElement | null; + if (targetEl?.closest?.("[data-dragify-user]")) return; + + const targetResolved = this.extractChannelIdFromTarget(event.target as HTMLElement | null); + const resolved = + channel && "id" in channel && channel.id + ? channel + : targetResolved; + if (!resolved?.id) return; + + const channelObj = ChannelStore.getChannel(resolved.id); + if (!channelObj) return; + const resolvedGuildId = resolved ? ("guild_id" in resolved ? resolved.guild_id : resolved.guildId) : undefined; + const guildId = resolvedGuildId ?? targetResolved?.guildId ?? channelObj?.guild_id; + const isDirectMessage = channelObj.isDM(); + if (isDirectMessage) { + const recipientId = this.getDmRecipientId(channelObj); + if (recipientId) { + this.onUserDragStart(event, { id: recipientId }); + return; + } + } + suppressDefaultDragPreview(event); + this.showGhost({ kind: "channel", id: channelObj.id, guildId }, event); + const payload = JSON.stringify({ kind: "channel", id: channelObj.id, guildId }); + activeDragEntity = { kind: "channel", id: channelObj.id, guildId }; + dragifyActive = true; + lastDragEventAt = Date.now(); + setDragifyDataTransfer(event.dataTransfer ?? null, payload); + }, + + onUserDragStart(event: DragEvent, user?: { id: string; userId?: string; user?: { id: string; }; }) { + if (this.isAttachmentElement(event.target as HTMLElement | null)) return; + const searchTarget = event.target as HTMLElement | null; + if (searchTarget?.closest) { + const chatMessage = searchTarget.closest("[data-author-id]") as HTMLElement | null; + if (chatMessage) { + const authorId = chatMessage.getAttribute("data-author-id"); + const parsed = authorId ? extractSnowflakeFromString(authorId) : null; + if (parsed) { + user = { id: parsed }; + } + } + } + const currentTarget = (event as unknown as { currentTarget?: EventTarget | null; }).currentTarget as HTMLElement | null; + const target = (event.target as HTMLElement | null) ?? null; + const userIdFromParam = user?.id ?? user?.userId ?? user?.user?.id ?? null; + const userIdFromDom = + this.extractUserIdFromTarget(currentTarget) + ?? this.extractUserIdFromTarget(target) + ?? null; + const userIdFromEvent = this.extractUserIdFromEvent(event); + const rawUserId = + userIdFromParam + ?? userIdFromDom + ?? userIdFromEvent + ?? event.dataTransfer?.getData("data-user-id") + ?? null; + const userId = rawUserId ? (extractSnowflakeFromString(rawUserId) ?? rawUserId) : null; + if (!userId) return; + const payload = JSON.stringify({ kind: "user", id: userId }); + activeUserDragId = userId; + activeDragEntity = { kind: "user", id: userId }; + dragifyActive = true; + lastDragEventAt = Date.now(); + suppressDefaultDragPreview(event); + this.showGhost({ kind: "user", id: userId }, event); + setDragifyDataTransfer(event.dataTransfer ?? null, payload); + event.dataTransfer?.setData("data-user-id", userId); + }, + + onDmDragStart(event: DragEvent, channel?: Channel | null) { + let resolvedChannel = channel ?? null; + if (!resolvedChannel) { + const targetChannel = this.extractChannelIdFromTarget(event.target as HTMLElement | null); + if (targetChannel?.id) resolvedChannel = ChannelStore.getChannel(targetChannel.id) ?? null; + } + if (!resolvedChannel) return; + const shouldDragChannel = resolvedChannel.isGroupDM() && resolvedChannel.isMultiUserDM(); + if (!shouldDragChannel) { + const recipientId = this.getDmRecipientId(resolvedChannel); + if (recipientId) { + this.onUserDragStart(event, { id: recipientId }); + return; + } + } + + this.onChannelDragStart(event, resolvedChannel); + }, + + onGuildDragStart(event: DragEvent, guildId: string) { + suppressDefaultDragPreview(event); + this.showGhost({ kind: "guild", id: guildId }, event); + const payload = JSON.stringify({ kind: "guild", id: guildId }); + if (event.dataTransfer) event.dataTransfer.effectAllowed = "copyMove"; + activeGuildDragId = guildId; + activeDragEntity = { kind: "guild", id: guildId }; + dragifyActive = true; + lastDragEventAt = Date.now(); + setDragifyDataTransfer(event.dataTransfer ?? null, payload); + }, + + start() { + pluginInstance = this; + this.mountGhost(); + window.addEventListener("dragover", this.globalDragOver, true); + window.addEventListener("drop", this.globalDrop, true); + window.addEventListener("dragstart", this.globalDragStart, true); + window.addEventListener("drag", this.globalDragMove, true); + window.addEventListener("dragover", this.globalDragMove, true); + window.addEventListener("dragend", this.globalDragEnd, true); + if (dragStateWatchdog === null) { + dragStateWatchdog = window.setInterval(() => { + if (!dragifyActive) return; + if (Date.now() - lastDragEventAt < 1200) return; + activeUserDragId = null; + activeGuildDragId = null; + activeDragEntity = null; + dragifyActive = false; + hideDragGhost(); + }, 500); + } + }, + + stop() { + window.removeEventListener("dragover", this.globalDragOver, true); + window.removeEventListener("drop", this.globalDrop, true); + window.removeEventListener("dragstart", this.globalDragStart, true); + window.removeEventListener("drag", this.globalDragMove, true); + window.removeEventListener("dragover", this.globalDragMove, true); + window.removeEventListener("dragend", this.globalDragEnd, true); + clearDragState(); + inviteCache.clear(); + if (guildGhostCleanupTimer !== null) { + clearTimeout(guildGhostCleanupTimer); + guildGhostCleanupTimer = null; + } + if (dragStateWatchdog !== null) { + clearInterval(dragStateWatchdog); + dragStateWatchdog = null; + } + this.unmountGhost(); + pluginInstance = null; + }, + + globalDragOver: (event: DragEvent) => { + const inst = pluginInstance; + if (!inst || !inst.isMessageInputEvent(event)) return; + if (dragSourceIsInput) return; + if (event.dataTransfer?.files?.length) return; + const hasActiveGuildDrag = activeGuildDragId !== null; + const hasActiveEntity = activeDragEntity || activeUserDragId; + const shouldHandle = hasActiveGuildDrag || hasActiveEntity || dragifyActive || hasDragifyTransfer(event.dataTransfer); + if (!shouldHandle) return; + event.preventDefault(); + event.stopPropagation(); + if (event.dataTransfer) event.dataTransfer.dropEffect = "copy"; + }, + + globalDrop: async (event: DragEvent) => { + const inst = pluginInstance; + if (!inst || !inst.isMessageInputEvent(event)) return; + if (dragSourceIsInput) return; + if (event.dataTransfer?.files?.length) { + clearDragState(); + return; + } + const channelId = SelectedChannelStore.getChannelId(); + const channel = ChannelStore.getChannel(channelId); + if (!channel) return; + + const types = event.dataTransfer?.types ?? []; + const hasDragify = types.includes("application/dragify"); + const hasAnyActiveDrag = activeGuildDragId !== null || activeUserDragId !== null || activeDragEntity !== null || dragifyActive; + if (!hasDragify && !hasAnyActiveDrag) return; + + lastDropAt = Date.now(); + event.preventDefault(); + event.stopPropagation(); + await inst.onDrop(event, channel); + hideDragGhost(); + }, + + globalDragStart: (event: DragEvent) => { + const inst = pluginInstance; + if (!inst || !event.dataTransfer) return; + + const target = event.target as HTMLElement | null; + if (!target) return; + const hasDragify = event.dataTransfer.types?.includes("application/dragify") ?? false; + const path = event.composedPath?.() ?? []; + if (inst.isAttachmentElement(target)) return; + if (inst.isMessageInputElement(target)) { + clearDragState(); + dragSourceIsInput = true; + return; + } + for (const entry of path) { + const el = entry as Element | null; + if (!el) continue; + if (inst.isAttachmentElement(el)) return; + if (inst.isMessageInputElement(el)) { + clearDragState(); + dragSourceIsInput = true; + return; + } + } + + if (!hasDragify) { + for (const entry of path) { + const el = entry as HTMLElement | null; + if (!el) continue; + const userTarget = el.closest?.("[data-dragify-user]"); + if (!userTarget) continue; + const userId = inst.extractUserIdFromTarget(userTarget); + if (userId) { + inst.onUserDragStart(event, { id: userId }); + } + return; + } + } + + const authorContainer = target.closest?.("[data-author-id]"); + const authorId = authorContainer?.getAttribute?.("data-author-id") ?? null; + if (authorId && target.getAttribute?.("data-text") && !hasDragify) { + inst.onUserDragStart(event, { id: authorId }); + return; + } + + if (!hasDragify) { + const dragifyTarget = target.closest?.("[data-dragify-user]"); + if (dragifyTarget) { + const userIdFromTarget = inst.extractUserIdFromEvent(event) ?? inst.extractUserIdFromTarget(dragifyTarget); + if (userIdFromTarget) { + inst.onUserDragStart(event, { id: userIdFromTarget }); + return; + } + } + } + + if (!hasDragify) { + const avatarUserId = inst.extractAvatarUserIdFromTarget(target); + if (avatarUserId) { + const messageItem = target.closest?.("[data-author-id]") as HTMLElement | null; + const authorId = messageItem?.getAttribute("data-author-id") ?? null; + if (authorId) { + inst.onUserDragStart(event, { id: authorId }); + return; + } + const channelFromTarget = inst.extractChannelIdFromTarget(target); + if (channelFromTarget) { + inst.onChannelDragStart(event, channelFromTarget); + return; + } + } + } + + const channelFromTarget = inst.extractChannelIdFromTarget(target); + if (channelFromTarget && !hasDragify) { + if (target.closest?.("[data-user-id]")) return; + inst.onChannelDragStart(event, channelFromTarget); + return; + } + + const guildId = inst.extractGuildIdFromTarget(target); + if (!guildId) return; + + if (!hasDragify) { + event.dataTransfer.effectAllowed = "copyMove"; + suppressDefaultDragPreview(event); + activeGuildDragId = guildId; + setDragifyDataTransfer(event.dataTransfer, JSON.stringify({ kind: "guild", id: guildId })); + inst.showGhost({ kind: "guild", id: guildId }, event); + } + }, + globalDragEnd: (_event: DragEvent) => { + setTimeout(() => { + if (Date.now() - lastDropAt < 100) return; + clearDragState(); + hideDragGhost(); + }, 0); + }, + globalDragMove: (event: DragEvent | MouseEvent) => { + if (!isGhostVisible()) return; + if (event?.clientX == null || event?.clientY == null) return; + if (event instanceof DragEvent) { + lastDragEventAt = Date.now(); + if (activeGuildDragId !== null || activeDragEntity?.kind === "guild") { + if (guildGhostCleanupTimer !== null) clearTimeout(guildGhostCleanupTimer); + guildGhostCleanupTimer = window.setTimeout(() => { + if (Date.now() - lastDragEventAt < 200) return; + activeGuildDragId = null; + if (activeDragEntity?.kind === "guild") activeDragEntity = null; + hideDragGhost(); + }, 300); + } + } + scheduleDragGhostPosition(event.clientX + 16, event.clientY + 20); + }, + isMessageInputEvent(event: DragEvent): boolean { + const target = event.target as Element | null; + if (this.isMessageInputElement(target)) return true; + if (settings.store.allowChatBodyDrop && this.isChatBodyElement(target)) return true; + + const path = event.composedPath?.() ?? []; + for (const entry of path) { + const el = entry as Element | null; + if (this.isMessageInputElement(el)) return true; + if (settings.store.allowChatBodyDrop && this.isChatBodyElement(el)) return true; + } + + if (typeof document !== "undefined" && event?.clientX != null && event?.clientY != null) { + const elements = document.elementsFromPoint?.(event.clientX, event.clientY); + if (elements && elements.length) { + for (const el of elements) { + if (this.isMessageInputElement(el)) return true; + if (settings.store.allowChatBodyDrop && this.isChatBodyElement(el)) return true; + } + } else { + const atPoint = document.elementFromPoint(event.clientX, event.clientY); + if (this.isMessageInputElement(atPoint)) return true; + if (settings.store.allowChatBodyDrop && this.isChatBodyElement(atPoint)) return true; + } + } + + return false; + }, + + isMessageInputElement(el: Element | null): boolean { + if (!el) return false; + const selector = "[data-slate-editor],[role=\"textbox\"],[contenteditable=\"true\"],[aria-label^=\"Message \"]"; + return Boolean((el as HTMLElement).closest?.(selector)); + }, + + isChatBodyElement(el: Element | null): boolean { + if (!el) return false; + const selector = "[role=\"log\"],[data-list-id^=\"chat-messages\"]"; + return Boolean((el as HTMLElement).closest?.(selector)); + }, + + isAttachmentElement(target: Element | null): boolean { + let el: Element | null = target; + while (el) { + if (!(el instanceof HTMLElement)) { + el = el.parentElement ?? null; + continue; + } + const href = el.getAttribute("href") ?? ""; + const src = el.getAttribute("src") ?? ""; + const style = el.style?.backgroundImage ?? ""; + const dataAttachment = + el.getAttribute("data-attachment-id") + ?? el.getAttribute("data-attachment-type") + ?? el.getAttribute("data-attachment-item-id") + ?? ""; + + if (dataAttachment) return true; + + const combined = `${href} ${src} ${style}`; + if (/(?:cdn|media)\.discordapp\.(?:com|net)\/attachments\//i.test(combined)) return true; + if (/discord\.com\/attachments\//i.test(combined)) return true; + if (/cdn\.discordapp\.com\/ephemeral-attachments\//i.test(combined)) return true; + + el = el.parentElement; + } + return false; + }, + + extractGuildIdFromTarget(target: HTMLElement): string | null { + let el: HTMLElement | null = target; + while (el) { + const listId = el.getAttribute("data-list-id"); + const rawId = el.getAttribute("data-list-item-id") ?? ""; + const isGuildContext = (listId && /guild/i.test(listId)) || /guild/i.test(rawId); + + if (!isGuildContext) { + el = el.parentElement; + continue; + } + + const parts = rawId.split("___"); + const candidate = parts[parts.length - 1] ?? rawId; + if (/^\d{17,20}$/.test(candidate)) return candidate; + + const direct = + extractSnowflakeFromString(rawId) ?? + extractSnowflakeFromString(listId ?? "") ?? + extractSnowflakeFromString(el.getAttribute("data-guild-id") ?? ""); + if (direct) return direct; + + el = el.parentElement; + } + return null; + }, + + extractChannelIdFromTarget(target: HTMLElement | null): { id: string; guildId?: string; } | null { + let el = target; + while (el) { + const listId = el.getAttribute("data-list-id") ?? ""; + const rawId = el.getAttribute("data-list-item-id") ?? ""; + const channelIdAttr = el.getAttribute("data-channel-id") ?? el.getAttribute("data-item-id") ?? ""; + const threadIdAttr = el.getAttribute("data-thread-id") ?? ""; + const href = el.getAttribute("href") ?? ""; + const isChannelContext = /(channel|thread|private|forum)/i.test(listId) || /(channel|thread|private|forum)/i.test(rawId); + + const pathMatch = extractChannelPath(href); + if (pathMatch?.channelId) { + const guildId = pathMatch.guildId === "@me" ? undefined : pathMatch.guildId; + return { id: pathMatch.channelId, guildId }; + } + + const fullMatch = extractChannelFromUrl(href); + if (fullMatch?.channelId) { + const guildId = fullMatch.guildId === "@me" ? undefined : fullMatch.guildId; + return { id: fullMatch.channelId, guildId }; + } + + const candidate = isChannelContext + ? (extractSnowflakeFromString(threadIdAttr) + ?? extractSnowflakeFromString(channelIdAttr) + ?? extractSnowflakeFromString(rawId) + ?? extractSnowflakeFromString(listId)) + : null; + if (candidate) { + const guildId = extractSnowflakeFromString(el.getAttribute("data-guild-id") ?? "") ?? undefined; + return { id: candidate, guildId }; + } + + el = el.parentElement; + } + return null; + }, + + extractUserIdFromTarget(target: Element | null): string | null { + let el: Element | null = target; + while (el) { + if (!(el instanceof HTMLElement)) { + el = el.parentElement ?? null; + continue; + } + const dataUserId = el.getAttribute("data-user-id") ?? el.getAttribute("data-userid") ?? ""; + const dataAuthorId = el.getAttribute("data-author-id") ?? ""; + const listId = el.getAttribute("data-list-item-id") ?? el.getAttribute("data-item-id") ?? ""; + const href = el.getAttribute("href") ?? ""; + const src = el.getAttribute("src") ?? ""; + const aria = el.getAttribute("aria-label") ?? ""; + + const explicit = extractSnowflakeFromString(dataUserId) + ?? extractSnowflakeFromString(dataAuthorId); + if (explicit) return explicit; + + const listCandidate = extractSnowflakeFromString(listId); + if (listCandidate) { + if (UserStore.getUser(listCandidate)) return listCandidate; + const channel = ChannelStore.getChannel(listCandidate); + if (channel && channel.isDM() && !channel.isGroupDM() && !channel.isMultiUserDM()) { + const recipientId = this.getDmRecipientId(channel); + if (recipientId) return recipientId; + } + } + + const profile = extractUserFromProfile(href); + if (profile) return profile; + + const avatar = extractUserFromAvatar(src); + if (avatar) return avatar; + + const styleAttr = el.getAttribute("style") ?? ""; + const bgImage = el.style?.backgroundImage ?? ""; + const styleAvatar = extractUserFromAvatar(styleAttr + " " + bgImage); + if (styleAvatar) return styleAvatar; + + const ariaMatch = extractSnowflakeFromString(aria); + if (ariaMatch && UserStore.getUser(ariaMatch)) return ariaMatch; + + el = el.parentElement; + } + return null; + }, + + extractAvatarUserIdFromTarget(target: HTMLElement | null): string | null { + let el: Element | null = target; + while (el) { + if (!(el instanceof HTMLElement)) { + el = el.parentElement ?? null; + continue; + } + const src = el.getAttribute("src") ?? ""; + const styleAttr = el.getAttribute("style") ?? ""; + const bgImage = el.style?.backgroundImage ?? ""; + const candidate = extractUserFromAvatar(`${src} ${styleAttr} ${bgImage}`); + if (candidate) return candidate; + el = el.parentElement; + } + return null; + }, + + extractUserIdFromEvent(event: DragEvent): string | null { + const path = event.composedPath?.() ?? []; + for (const entry of path) { + const candidate = entry instanceof HTMLElement ? this.extractUserIdFromTarget(entry) : null; + if (candidate) return candidate; + } + + const target = event.target as HTMLElement | null; + if (target) { + const direct = target.closest?.("[data-author-id],[data-user-id]") as HTMLElement | null; + if (direct) { + return this.extractUserIdFromTarget(direct); + } + } + + if (typeof document !== "undefined" && event?.clientX != null && event?.clientY != null) { + const elements = document.elementsFromPoint?.(event.clientX, event.clientY); + if (elements && elements.length) { + for (const el of elements) { + const candidate = el instanceof HTMLElement ? this.extractUserIdFromTarget(el) : null; + if (candidate) return candidate; + } + } + } + + return null; + }, + + mountGhost() { + mountDragGhost(); + }, + + unmountGhost() { + unmountDragGhost(); + }, + + showGhost(entity: DropEntity, event?: DragEvent) { + const ghost = this.buildGhost(entity); + if (!ghost) return; + const position = event && event?.clientX != null && event?.clientY != null + ? { x: event.clientX + 16, y: event.clientY + 20 } + : undefined; + showDragGhost({ ...ghost, entityId: entity.id }, position); + }, + + buildGhost(entity: DropEntity): Omit | null { + if (entity.kind === "user") { + const user = UserStore.getUser(entity.id); + if (!user) return null; + const title = user.globalName ?? user.username; + const subtitle = user.username ? `@${user.username}` : "User"; + const iconUrl = user.getAvatarURL(void 0, 80, true) ?? undefined; + return { kind: "user", title, subtitle, iconUrl, symbol: "@", badge: "user", entityId: entity.id, exiting: false }; + } + + if (entity.kind === "channel") { + const channel = ChannelStore.getChannel(entity.id); + if (!channel) return null; + const isThread = channel.isThread(); + const isGroupDm = channel.type === ChannelType.GROUP_DM; + const channelName = this.getGroupDmDisplayName(channel) || channel.name; + const title = `${isThread || isGroupDm ? "" : "#"}${channelName}`; + const guild = channel?.guild_id ? GuildStore.getGuild(channel.guild_id) : null; + const subtitle = guild?.name ?? (entity.guildId ? "Server" : "Direct Messages"); + let iconUrl: string | undefined; + if (guild?.icon) { + iconUrl = IconUtils.getGuildIconURL({ id: guild.id, icon: guild.icon, size: 64 }) ?? undefined; + } else if (channel.type === ChannelType.GROUP_DM) { + const channelIcon = IconUtils.getChannelIconURL?.({ + id: channel.id, + icon: (channel as { icon?: string | null; }).icon ?? undefined, + size: 64, + }); + iconUrl = channelIcon ?? undefined; + } + if (!iconUrl && (channel.isDM() || channel.type === ChannelType.GROUP_DM)) { + const recipientId = this.getDmRecipientId(channel); + const recipient = recipientId ? UserStore.getUser(recipientId) : null; + iconUrl = recipient ? recipient.getAvatarURL(void 0, 80, true) : undefined; + } + let badge = "channel"; + if (isThread) { + badge = "thread"; + } else { + switch (channel.type) { + case ChannelType.GUILD_VOICE: + case ChannelType.GUILD_STAGE_VOICE: + badge = "voice"; + break; + case ChannelType.GUILD_FORUM: + badge = "forum"; + break; + case ChannelType.GUILD_MEDIA: + badge = "media"; + break; + case ChannelType.GUILD_ANNOUNCEMENT: + badge = "announcement"; + break; + case ChannelType.DM: + case ChannelType.GROUP_DM: + badge = "dm"; + break; + default: + badge = "channel"; + } + } + return { kind: "channel", title, subtitle, iconUrl, badge, entityId: entity.id, exiting: false }; + } + + const guild = GuildStore.getGuild(entity.id); + const title = guild?.name ?? "Server"; + const subtitle = "Server"; + const iconUrl = guild?.icon + ? IconUtils.getGuildIconURL({ id: guild.id, icon: guild.icon, size: 64 }) ?? undefined + : undefined; + const symbol = guild ? getGuildAcronym(guild) : "S"; + return { kind: "guild", title, subtitle, iconUrl, symbol, badge: "server", entityId: entity.id, exiting: false }; + }, + + getDmRecipientId(channel?: Channel | null): string | null { + if (!channel) return null; + return channel.getRecipientId() + ?? channel.recipients?.[0] + ?? channel.rawRecipients?.[0] + ?? null; + }, + + getGroupDmDisplayName(channel?: Channel | null): string | null { + if (!channel || channel.type !== ChannelType.GROUP_DM) return null; + const selfId = UserStore.getCurrentUser()?.id ?? null; + const recipients = (channel.recipients ?? channel.rawRecipients ?? []).filter(Boolean); + if (recipients.length === 0) return null; + const names = recipients + .filter(id => id !== selfId) + .map(id => { + const user = UserStore.getUser(id); + return RelationshipStore?.getNickname?.(id) + ?? user?.globalName + ?? user?.username + ?? null; + }) + .filter(Boolean); + return names.length > 1 ? names.join(", ") : null; + }, +}); diff --git a/src/equicordplugins/dragify/style.css b/src/equicordplugins/dragify/style.css new file mode 100644 index 000000000..c24a1d2d4 --- /dev/null +++ b/src/equicordplugins/dragify/style.css @@ -0,0 +1,149 @@ +.vc-dragify-ghost-container { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9999; +} + +.vc-dragify-ghost { + position: fixed; + top: 0; + left: 0; + pointer-events: none; + color: var(--text-normal, #fff); + opacity: 1; + animation: vc-dragify-fade-in 200ms ease-out forwards; + will-change: transform; +} + +.vc-dragify-ghost-exit { + animation: vc-dragify-fade-out 200ms ease-in forwards; +} + +.vc-dragify-card { + display: flex; + align-items: center; + gap: 12px; + padding: 6px 10px; + min-width: 220px; + max-width: 320px; + border-radius: 6px; + background: rgb(0 0 0 / 70%); + border: 1px solid rgb(255 255 255 / 10%); + box-shadow: 0 12px 40px rgb(0 0 0 / 35%); + backdrop-filter: blur(12px); + transform: translateZ(0); + color: var(--text-default, #fff); + font-family: var(--font-code); + line-height: 1.4; + animation: none; + will-change: transform; +} + +.vc-dragify-icon { + width: 28px; + height: 28px; + border-radius: 6px; + background: rgb(255 255 255 / 10%); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; + color: var(--text-default, #fff); + font-weight: 600; +} + +.vc-dragify-icon-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.vc-dragify-icon-text { + font-size: 13px; + color: var(--text-default, #fff); +} + +.vc-dragify-body { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1; +} + +.vc-dragify-title-row { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.vc-dragify-title { + font-size: 14px; + font-weight: 600; + color: var(--text-default, #fff); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.vc-dragify-voice-icon { + flex-shrink: 0; + color: var(--interactive-normal, #b9bbbe); +} + +.vc-dragify-voice-icon-muted { + color: var(--status-danger, #ed4245); +} + +.vc-dragify-voice-icon-stream { + color: var(--status-positive, #3ba55d); +} + +.vc-dragify-subtitle { + font-size: 11px; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.vc-dragify-badge { + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + margin-left: auto; +} + +@keyframes vc-dragify-fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes vc-dragify-fade-out { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .vc-dragify-ghost { + animation: none; + } + + .vc-dragify-ghost-exit { + animation: none; + opacity: 0; + } +} diff --git a/src/equicordplugins/dragify/utils.ts b/src/equicordplugins/dragify/utils.ts new file mode 100644 index 000000000..131f0db28 --- /dev/null +++ b/src/equicordplugins/dragify/utils.ts @@ -0,0 +1,182 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import type { Channel, Guild, User } from "@vencord/discord-types"; + +const userMentionRegex = /<@!?(\d{17,20})>/; +const userProfileUrlRegex = /discord(?:(?:app)?\.com|:\/\/-?)\/users\/(\d{17,20})/; +const userAvatarRegex = /cdn\.discordapp\.com\/(?:avatars|users)\/(\d{17,20})\//; +const guildUserAvatarRegex = /cdn\.discordapp\.com\/guilds\/\d{17,20}\/users\/(\d{17,20})\/avatars\//; +const channelMentionRegex = /<#(\d{17,20})>/; +const channelUrlRegex = /discord(?:(?:app)?\.com|:\/\/-?)\/channels\/(?:(@me)|(\d{17,20}))\/(\d{17,20})/; +const channelPathRegex = /\/channels\/(@me|\d{17,20})\/(\d{17,20})/; +const guildIconRegex = /cdn\.discordapp\.com\/icons\/(\d{17,20})\//; + +export type DropEntity = + | { kind: "user"; id: string; } + | { kind: "channel"; id: string; guildId?: string; } + | { kind: "guild"; id: string; }; + +interface DragifyPayload { + id?: string; + userId?: string; + channelId?: string; + guildId?: string; + type?: string; + kind?: string; + itemType?: string; +} + +type StoreSet = { + ChannelStore: { getChannel(id: string): Channel | null | undefined; }; + GuildStore: { getGuild(id: string): Guild | null | undefined; }; + UserStore: { getUser(id: string): User | null | undefined; }; +}; + +export function tryParseJson>(value: string): T | null { + if (!value || value.length < 2 || (value[0] !== "{" && value[0] !== "[")) return null; + try { + const parsed = JSON.parse(value); + return typeof parsed === "object" && parsed ? parsed as T : null; + } catch { + return null; + } +} + +export function extractSnowflakeFromString(value: string): string | null { + const match = value.match(/\d{17,20}/); + return match?.[0] ?? null; +} + +export function extractSnowflakes(values: string[]): string[] { + const ids = new Set(); + const regex = /\d{17,20}/g; + for (const value of values) { + const matches = value.match(regex); + if (matches) matches.forEach(id => ids.add(id)); + } + return Array.from(ids); +} + +export function extractStrings(dataTransfer: DataTransfer): string[] { + const collected = new Set(); + const add = (v?: string) => v && collected.add(v); + add(dataTransfer.getData("text/plain")); + add(dataTransfer.getData("text/uri-list")); + add(dataTransfer.getData("text/html")); + add(dataTransfer.getData("text/x-moz-url")); + add(dataTransfer.getData("application/json")); + for (const type of dataTransfer.types ?? []) add(dataTransfer.getData(type)); + + const split: string[] = []; + for (const v of collected) { + split.push(v); + if (v.includes("\n")) split.push(...v.split(/\s+/).filter(Boolean)); + } + return Array.from(new Set(split)); +} + +export async function collectPayloadStrings(dataTransfer: DataTransfer): Promise { + const sync = extractStrings(dataTransfer); + const asyncValues: string[] = []; + const itemPromises = Array.from(dataTransfer.items ?? []) + .filter(item => item.kind === "string") + .map(item => new Promise(resolve => item.getAsString(val => { + if (val) asyncValues.push(val); + resolve(); + }))); + await Promise.all(itemPromises); + return Array.from(new Set([...sync, ...asyncValues])); +} + +export function parseFromStrings(payloads: string[], stores: StoreSet): DropEntity | null { + if (payloads.length === 0) return null; + + for (const value of payloads) { + const parsed = tryParseJson(value); + if (parsed) { + const id = parsed.id ?? parsed.userId ?? parsed.channelId ?? parsed.guildId; + const type = (parsed.type ?? parsed.kind ?? parsed.itemType ?? "").toLowerCase(); + if (id) { + if (type.includes("user")) return { kind: "user", id }; + if (type.includes("channel")) return { kind: "channel", id, guildId: parsed.guildId }; + if (type.includes("guild") || type.includes("server")) return { kind: "guild", id }; + } + } + } + + for (const value of payloads) { + const trimmed = value.trim(); + if (!trimmed) continue; + const userFromMention = userMentionRegex.exec(trimmed); + if (userFromMention) return { kind: "user", id: userFromMention[1] }; + const userFromProfile = userProfileUrlRegex.exec(trimmed); + if (userFromProfile) return { kind: "user", id: userFromProfile[1] }; + const guildUserAvatar = guildUserAvatarRegex.exec(trimmed); + if (guildUserAvatar) return { kind: "user", id: guildUserAvatar[1] }; + const userFromAvatar = userAvatarRegex.exec(trimmed); + if (userFromAvatar) return { kind: "user", id: userFromAvatar[1] }; + } + + for (const value of payloads) { + const trimmed = value.trim(); + if (!trimmed) continue; + const channelFromMention = channelMentionRegex.exec(trimmed); + if (channelFromMention) return { kind: "channel", id: channelFromMention[1] }; + const channelFromUrl = channelUrlRegex.exec(trimmed); + if (channelFromUrl) { + const guildId = channelFromUrl[1] === "@me" ? "@me" : channelFromUrl[2]; + return { kind: "channel", id: channelFromUrl[3], guildId: guildId ?? undefined }; + } + } + + for (const value of payloads) { + const trimmed = value.trim(); + if (!trimmed) continue; + const guildFromIcon = guildIconRegex.exec(trimmed); + if (guildFromIcon) return { kind: "guild", id: guildFromIcon[1] }; + } + + const candidates = extractSnowflakes(payloads); + for (const candidate of candidates) { + if (stores.ChannelStore.getChannel(candidate)) return { kind: "channel", id: candidate }; + if (stores.GuildStore.getGuild(candidate)) return { kind: "guild", id: candidate }; + if (stores.UserStore.getUser(candidate)) return { kind: "user", id: candidate }; + } + return null; +} + +export function extractChannelPath(value: string): { guildId?: string; channelId?: string; } | null { + const channelPath = channelPathRegex.exec(value); + if (!channelPath) return null; + return { + guildId: channelPath[1] === "@me" ? "@me" : channelPath[1], + channelId: channelPath[2] + }; +} + +export function extractChannelFromUrl(value: string): { guildId?: string; channelId?: string; } | null { + const channelFromUrl = channelUrlRegex.exec(value); + if (!channelFromUrl) return null; + const guildId = channelFromUrl[1] ?? channelFromUrl[2]; + return { + guildId: guildId === "@me" ? "@me" : guildId, + channelId: channelFromUrl[3] + }; +} + +export function extractUserFromAvatar(value: string): string | null { + const guildUserAvatar = guildUserAvatarRegex.exec(value); + if (guildUserAvatar?.[1]) return guildUserAvatar[1]; + const userAvatar = userAvatarRegex.exec(value); + if (userAvatar?.[1]) return userAvatar[1]; + return null; +} + +export function extractUserFromProfile(value: string): string | null { + const match = userProfileUrlRegex.exec(value); + return match?.[1] ?? null; +} diff --git a/src/equicordplugins/ingtoninator/index.tsx b/src/equicordplugins/ingtoninator/index.tsx index 7cf92b2e2..43013fc4f 100644 --- a/src/equicordplugins/ingtoninator/index.tsx +++ b/src/equicordplugins/ingtoninator/index.tsx @@ -1,4 +1,4 @@ - /* +/* * Vencord, a Discord client mod * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later @@ -94,7 +94,7 @@ const handleMessage = ((channelId, message) => { const wordMatch: WordMatch | null = chooseRandomWord(msg); if (wordMatch === null) return; - const word = wordMatch.word; + const { word } = wordMatch; const wordUpper = word.toUpperCase(); const isUpper = word === wordUpper; From 3d4cf25659e726ac5ab210a66686a0a0baf17060 Mon Sep 17 00:00:00 2001 From: sadan4 <117494111+sadan4@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:50:16 -0500 Subject: [PATCH 3/6] fix remaining things broken by recent Discord update (#3966) Co-authored-by: V Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/Vencord.ts | 4 +- src/plugins/_api/dynamicImageModalApi.ts | 3 +- src/plugins/_api/messageUpdater.ts | 2 +- src/plugins/betterGifAltText/index.ts | 2 +- src/plugins/betterGifPicker/index.ts | 16 ++++-- src/plugins/betterSessions/index.tsx | 7 ++- src/plugins/betterSettings/index.tsx | 10 ++-- src/plugins/blurNsfw/index.ts | 2 +- src/plugins/consoleJanitor/index.tsx | 52 +++++++++++++------ src/plugins/consoleShortcuts/index.ts | 12 +++-- src/plugins/customRPC/index.tsx | 2 +- src/plugins/decor/index.tsx | 9 +++- .../decor/ui/modals/CreateDecorationModal.tsx | 2 +- src/plugins/experiments/index.tsx | 5 +- src/plugins/fakeNitro/index.tsx | 4 +- src/plugins/fullSearchContext/index.tsx | 2 +- src/plugins/fullUserInChatbox/index.tsx | 2 +- src/plugins/gifPaste/index.ts | 6 +-- src/plugins/imageZoom/index.tsx | 2 +- src/plugins/loadingQuotes/index.ts | 4 +- src/plugins/mentionAvatars/index.tsx | 14 +++-- src/plugins/messageLogger/index.tsx | 2 +- src/plugins/normalizeMessageLinks/index.ts | 26 ---------- src/plugins/pinDms/index.tsx | 10 ++-- src/plugins/platformIndicators/index.tsx | 6 +-- src/plugins/revealAllSpoilers/index.ts | 2 +- .../reviewDB/components/ReviewsView.tsx | 2 +- src/plugins/roleColorEverywhere/index.tsx | 2 +- src/plugins/showHiddenChannels/index.tsx | 19 ++++--- src/plugins/showHiddenThings/index.ts | 2 +- src/plugins/spotifyCrack/index.ts | 2 +- src/plugins/themeAttributes/index.ts | 14 +++-- src/plugins/typingTweaks/index.tsx | 2 +- src/plugins/userMessagesPronouns/index.ts | 2 +- src/plugins/usrbg/index.tsx | 4 +- src/plugins/validReply/index.ts | 14 +++-- src/plugins/viewIcons/index.tsx | 14 +++-- src/plugins/webContextMenus.web/index.ts | 6 +-- src/plugins/webKeybinds.web/index.ts | 2 +- src/plugins/webScreenShareFixes.web/index.ts | 10 +++- src/plugins/whoReacted/index.tsx | 2 +- src/webpack/common/components.ts | 2 +- src/webpack/common/utils.ts | 4 +- 43 files changed, 179 insertions(+), 132 deletions(-) delete mode 100644 src/plugins/normalizeMessageLinks/index.ts diff --git a/src/Vencord.ts b/src/Vencord.ts index d451e9f28..86a2e8e34 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -34,6 +34,7 @@ import { debounce } from "@shared/debounce"; import { IS_WINDOWS } from "@utils/constants"; import { createAndAppendStyle } from "@utils/css"; import { StartAt } from "@utils/types"; +import { SettingsRouter } from "@webpack/common"; import { get as dsGet } from "./api/DataStore"; import { NotificationData, showNotification } from "./api/Notifications"; @@ -44,7 +45,6 @@ import { localStorage } from "./utils/localStorage"; import { relaunch } from "./utils/native"; import { checkForUpdates, update, UpdateLogger } from "./utils/updater"; import { onceReady } from "./webpack"; -import { SettingsRouter } from "./webpack/common"; import { patches } from "./webpack/patchWebpack"; if (IS_REPORTER) { @@ -68,7 +68,7 @@ async function syncSettings() { body: "We've noticed you have cloud integrations enabled in another client! Due to limitations, you will " + "need to re-authenticate to continue using them. Click here to go to the settings page to do so!", color: "var(--yellow-360)", - onClick: () => SettingsRouter.open("VencordCloud") + onClick: () => SettingsRouter.openUserSettings("vencord_cloud_panel") }); return; } diff --git a/src/plugins/_api/dynamicImageModalApi.ts b/src/plugins/_api/dynamicImageModalApi.ts index 775cdf030..4cbd95c9a 100644 --- a/src/plugins/_api/dynamicImageModalApi.ts +++ b/src/plugins/_api/dynamicImageModalApi.ts @@ -14,7 +14,8 @@ export default definePlugin({ description: "Allows you to omit either width or height when opening an image modal", patches: [ { - find: ".renderLinkComponent)?", + // TODO: bundler compat + find: ".renderLinkComponent", replacement: { // widthAndHeightPassed = w != null && w !== 0 && h == null || h === 0 match: /(?<=\i=)(null!=\i&&0!==\i)&&(null!=\i&&0!==\i)/, diff --git a/src/plugins/_api/messageUpdater.ts b/src/plugins/_api/messageUpdater.ts index 8f6cca26a..d029456e7 100644 --- a/src/plugins/_api/messageUpdater.ts +++ b/src/plugins/_api/messageUpdater.ts @@ -27,7 +27,7 @@ export default definePlugin({ patches: [ { // Message accessories have a custom logic to decide if they should render again, so we need to make it not ignore changed message reference - find: "}renderEmbeds(", + find: "}renderStickersAccessories(", replacement: { match: /(?<=this.props,\i,\[)"message",/, replace: "" diff --git a/src/plugins/betterGifAltText/index.ts b/src/plugins/betterGifAltText/index.ts index 5d9b188ea..0a381e547 100644 --- a/src/plugins/betterGifAltText/index.ts +++ b/src/plugins/betterGifAltText/index.ts @@ -26,7 +26,7 @@ export default definePlugin({ "Change GIF alt text from simply being 'GIF' to containing the gif tags / filename", patches: [ { - find: '"onCloseImage",', + find: ".modalContext})};", replacement: { match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/, replace: diff --git a/src/plugins/betterGifPicker/index.ts b/src/plugins/betterGifPicker/index.ts index 9d7d8db41..bc39c6b5c 100644 --- a/src/plugins/betterGifPicker/index.ts +++ b/src/plugins/betterGifPicker/index.ts @@ -13,11 +13,17 @@ export default definePlugin({ authors: [Devs.Samwich], patches: [ { - find: '"state",{resultType:', - replacement: [{ - match: /(?<="state",{resultType:)null/, - replace: '"Favorites"' - }] + find: "renderHeaderContent(){", + replacement: [ + { + match: /(?<="state",{resultType:)null/, + replace: '"Favorites"' + }, + { + match: /(?<=state={resultType:)null/, + replace: '"Favorites"' + } + ] } ] }); diff --git a/src/plugins/betterSessions/index.tsx b/src/plugins/betterSessions/index.tsx index 78aea6133..b6dabbaa0 100644 --- a/src/plugins/betterSessions/index.tsx +++ b/src/plugins/betterSessions/index.tsx @@ -23,15 +23,14 @@ import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy, findStoreLazy } from "@webpack"; -import { Constants, React, RestAPI, Tooltip } from "@webpack/common"; +import { findComponentByCodeLazy, findCssClassesLazy, findStoreLazy } from "@webpack"; +import { Constants, React, RestAPI, SettingsRouter, Tooltip } from "@webpack/common"; import { RenameButton } from "./components/RenameButton"; import { Session, SessionInfo } from "./types"; import { fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils"; const AuthSessionsStore = findStoreLazy("AuthSessionsStore"); -const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open"); const TimestampClasses = findCssClassesLazy("timestamp", "blockquoteContainer"); const SessionIconClasses = findCssClassesLazy("sessionIcon"); @@ -175,7 +174,7 @@ export default definePlugin({ title: "BetterSessions", body: `New session:\n${session.client_info.os} · ${session.client_info.platform} · ${session.client_info.location}`, permanent: true, - onClick: () => UserSettingsModal.open("Sessions") + onClick: () => SettingsRouter.openUserSettings("sessions_panel") }); } diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index 2236d2fdc..79fe68509 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -100,9 +100,9 @@ export default definePlugin({ { find: "this.renderArtisanalHack()", replacement: [ - { // Fade in on layer - match: /(?<=\((\i),"contextType",\i\.\i\);)/, - replace: "$1=$self.Layer;", + { + match: /class (\i)(?= extends \i\.PureComponent.+?static contextType=.+?jsx\)\(\1,\{mode:)/, + replace: "var $1=$self.Layer;class VencordPatchedOldFadeLayer", predicate: () => settings.store.disableFade }, { // Lazy-load contents @@ -129,8 +129,8 @@ export default definePlugin({ { // Load menu TOC eagerly find: "#{intl::USER_SETTINGS_WITH_BUILD_OVERRIDE}", replacement: { - match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)).*?,(?=\1\(this)/, - replace: "$&(async ()=>$2)()," + match: /(?=handleOpenSettingsContextMenu=.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)))/, + replace: "_vencordBetterSettingsEagerLoad=(async ()=>$1)();" }, predicate: () => settings.store.eagerLoad }, diff --git a/src/plugins/blurNsfw/index.ts b/src/plugins/blurNsfw/index.ts index 358c986b0..1681f8c57 100644 --- a/src/plugins/blurNsfw/index.ts +++ b/src/plugins/blurNsfw/index.ts @@ -45,7 +45,7 @@ export default definePlugin({ patches: [ { - find: "}renderEmbeds(", + find: "}renderStickersAccessories(", replacement: [ { match: /(\.renderReactions\(\i\).+?className:)/, diff --git a/src/plugins/consoleJanitor/index.tsx b/src/plugins/consoleJanitor/index.tsx index 038f8513a..067fa270f 100644 --- a/src/plugins/consoleJanitor/index.tsx +++ b/src/plugins/consoleJanitor/index.tsx @@ -147,10 +147,16 @@ export default definePlugin({ }, { find: "is not a valid locale.", - replacement: { - match: /\i\.error(?=\(""\.concat\(\i," is not a valid locale."\)\))/, - replace: "$self.Noop" - } + replacement: [ + { + match: /\i\.error(?=\(""\.concat\(\i," is not a valid locale."\)\))/, + replace: "$self.Noop" + }, + { + match: /\i\.error(?=\(`\$\{\i\} is not a valid locale.`)/, + replace: "$self.Noop" + } + ] }, { find: '"AppCrashedFatalReport: getLastCrash not supported."', @@ -161,10 +167,16 @@ export default definePlugin({ }, { find: "RPCServer:WSS", - replacement: { - match: /\i\.error\("Error: "\.concat\((\i)\.message/, - replace: '!$1.message.includes("EADDRINUSE")&&$&' - } + replacement: [ + { + match: /\i\.error\("Error: "\.concat\((\i)\.message/, + replace: '!$1.message.includes("EADDRINUSE")&&$&' + }, + { + match: /\i\.error\(`Error: \$\{(\i)\.message\}/, + replace: '!$1.message.includes("EADDRINUSE")&&$&' + } + ] }, { find: "Tried getting Dispatch instance before instantiated", @@ -182,17 +194,25 @@ export default definePlugin({ }, { find: "failed to send analytics events", - replacement: { - match: /console\.error\("\[analytics\] failed to send analytics events query: "\.concat\(\i\)\)/, - replace: "" - } + replacement: [ + { + match: /console\.error\("\[analytics\] failed to send analytics events query: "\.concat\(\i\)\)/, + replace: "" + }, + { + match: /console\.error\(`\[analytics\] failed to send analytics events query: \$\{\i\}`\)/, + replace: "" + } + ] }, { find: "Slow dispatch on", - replacement: { - match: /\i\.totalTime>\i&&\i\.verbose\("Slow dispatch on ".+?\)\);/, - replace: "" - } + replacement: [ + { + match: /\i\.totalTime>\i&&\i\.verbose\([`"]Slow dispatch on.{0,55}\);/, + replace: "" + }, + ] }, // Patches Discord generic logger function { diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index c163dd649..3b35e89e4 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -247,11 +247,13 @@ export default definePlugin({ patches: [ { - find: 'this,"_changeCallbacks",', - replacement: { - match: /\i\(this,"_changeCallbacks",/, - replace: "Reflect.defineProperty(this,Symbol.toStringTag,{value:this.getName(),configurable:!0,writable:!0,enumerable:!1}),$&" - } + find: "&&this.initializeIfNeeded()", + replacement: [ + { + match: /\i&&this\.initializeIfNeeded\(\)/, + replace: "$&,Reflect.defineProperty(this,Symbol.toStringTag,{value:this.getName(),configurable:!0,writable:!0,enumerable:!1})" + } + ] } ], diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx index 8e3328e80..d41c904d0 100644 --- a/src/plugins/customRPC/index.tsx +++ b/src/plugins/customRPC/index.tsx @@ -225,7 +225,7 @@ export default definePlugin({ // Discord hides buttons on your own Rich Presence for some reason. This patch disables that behaviour patches: [ { - find: ".buttons.length)>=1", + find: ".USER_PROFILE_ACTIVITY_BUTTONS),", replacement: { match: /.getId\(\)===\i.id/, replace: "$& && false" diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 6e8e53f16..c4888b94b 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -50,7 +50,7 @@ export default definePlugin({ find: "80,onlyAnimateOnHoverOrFocus:!", replacement: [ { - match: /(?<==)\i=>{let{children.{20,200}isSelected:\i=!1\}=\i/, + match: /(?<==)\i=>{let{children.{20,200}isSelected:\i=!1.{0,5}\}=\i/, replace: "$self.DecorationGridItem=$&", }, { @@ -106,6 +106,10 @@ export default definePlugin({ { match: /(?<=userValue.{0,25}void 0:)((\i)\.avatarDecoration)/, replace: "$self.useUserDecorAvatarDecoration($2)??$1" + }, + { + match: /(?<=userValue:)((\i)(?:.{0,10})?\.avatarDecoration)/, + replace: "$self.useUserDecorAvatarDecoration($2)??$1" } ] })), @@ -114,7 +118,7 @@ export default definePlugin({ find: "#{intl::PREMIUM_UPSELL_PROFILE_AVATAR_DECO_INLINE_UPSELL_DESCRIPTION}", replacement: { match: /(#{intl::PREMIUM_UPSELL_PROFILE_AVATAR_DECO_INLINE_UPSELL_DESCRIPTION}.+?return null!=(\i)&&\()(null==\i)/, - replace: (_, rest, avatarDecoration, hasPurchase) => `${rest}(${avatarDecoration}.skuId!==$self.SKU_ID&&${hasPurchase})` + replace: (_, rest, avatarDecoration, hasPurchase) => `${rest}(${avatarDecoration}.skuId!==$self.SKU_ID&&${avatarDecoration}.skuId!==$self.RAW_SKU_ID&&${hasPurchase})` } } ], @@ -140,6 +144,7 @@ export default definePlugin({ }, SKU_ID, + RAW_SKU_ID, useUserDecorAvatarDecoration, diff --git a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx index f81135977..fa07cc312 100644 --- a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx @@ -16,7 +16,7 @@ import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeade import { filters, findComponentByCodeLazy, mapMangledModuleLazy } from "@webpack"; import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common"; -const FileUpload = findComponentByCodeLazy(".currentTarget.files)", "lineClamp:1"); +const FileUpload = findComponentByCodeLazy(".currentTarget.files", "lineClamp:1"); const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE="positive', { HelpMessageTypes: filters.byProps("POSITIVE", "WARNING", "INFO"), diff --git a/src/plugins/experiments/index.tsx b/src/plugins/experiments/index.tsx index ab97ae92d..df750cdc6 100644 --- a/src/plugins/experiments/index.tsx +++ b/src/plugins/experiments/index.tsx @@ -30,7 +30,7 @@ import { Forms, React } from "@webpack/common"; import hideBugReport from "./hideBugReport.css?managed"; const KbdStyles = findByPropsLazy("key", "combo"); -const BugReporterExperiment = findLazy(m => m?.definition?.id === "2024-09_bug_reporter"); +const BugReporterExperiment = findLazy(m => m?.definition?.name === "2026-01-bug-reporter"); const modKey = IS_MAC ? "cmd" : "ctrl"; const altKey = IS_MAC ? "opt" : "alt"; @@ -109,6 +109,7 @@ export default definePlugin({ find: "Clear Treatment ", replacement: [ { + // TODO: stable compat optional chaining remove once some time has passed match: /\i\??\.isStaff\(\)/, replace: "true" }, @@ -129,7 +130,7 @@ export default definePlugin({ } ], - start: () => !BugReporterExperiment.getCurrentConfig().hasBugReporterAccess && enableStyle(hideBugReport), + start: () => !BugReporterExperiment.getConfig().hasBugReporterAccess && enableStyle(hideBugReport), stop: () => disableStyle(hideBugReport), settingsAboutComponent: () => { diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 4a943e118..13a5b5e82 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -293,13 +293,13 @@ export default definePlugin({ { // Patch the rendered message content to add fake nitro emojis or remove sticker links predicate: () => settings.store.transformEmojis || settings.store.transformStickers, - match: /(?=return{hasSpoilerEmbeds:\i,content:(\i))/, + match: /(?=return{hasSpoilerEmbeds:\i,hasBailedAst:\i,content:(\i))/, replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);` } ] }, { - find: "}renderEmbeds(", + find: "}renderStickersAccessories(", replacement: [ { // Call our function to decide whether the embed should be ignored or not diff --git a/src/plugins/fullSearchContext/index.tsx b/src/plugins/fullSearchContext/index.tsx index d5b9c8a1e..2f20cd53b 100644 --- a/src/plugins/fullSearchContext/index.tsx +++ b/src/plugins/fullSearchContext/index.tsx @@ -34,7 +34,7 @@ interface CopyIdMenuItemProps { } let CopyIdMenuItem: (props: CopyIdMenuItemProps) => React.ReactElement | null = NoopComponent; -waitFor(filters.componentByCode('"devmode-copy-id-".concat'), m => CopyIdMenuItem = m); +waitFor(filters.componentByCode('"cannot copy null text"'), m => CopyIdMenuItem = m); function MessageMenu({ message, channel, onHeightUpdate }) { const canReport = message.author && diff --git a/src/plugins/fullUserInChatbox/index.tsx b/src/plugins/fullUserInChatbox/index.tsx index a9d4d61d9..9ac4f811d 100644 --- a/src/plugins/fullUserInChatbox/index.tsx +++ b/src/plugins/fullUserInChatbox/index.tsx @@ -28,7 +28,7 @@ export default definePlugin({ patches: [ { // Same find as RoleColorEverywhere chatbox mentions - find: ':"text":', + find: '"text":"locked"', replacement: { match: /(hidePersonalInformation\).+?)(if\(null!=\i\){.+?return \i)(?=})/, replace: "$1return $self.UserMentionComponent({...arguments[0],originalComponent:()=>{$2}});" diff --git a/src/plugins/gifPaste/index.ts b/src/plugins/gifPaste/index.ts index b2b4305d8..4e520d122 100644 --- a/src/plugins/gifPaste/index.ts +++ b/src/plugins/gifPaste/index.ts @@ -27,10 +27,10 @@ export default definePlugin({ authors: [Devs.Ven], patches: [{ - find: '"handleSelectGIF",', + find: "handleSelectGIF=", replacement: { - match: /"handleSelectGIF",(\i)=>\{/, - replace: '"handleSelectGIF",$1=>{if (!this.props.className) return $self.handleSelect($1);' + match: /handleSelectGIF=(\i)=>\{/, + replace: "$&if (!this.props.className) return $self.handleSelect($1);" } }], diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 061407411..4bdfe4378 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -163,7 +163,7 @@ export default definePlugin({ patches: [ { - find: ".renderLinkComponent)?", + find: "disableArrowKeySeek:!0", replacement: [ { match: /useFullWidth:!0,shouldLink:/, diff --git a/src/plugins/loadingQuotes/index.ts b/src/plugins/loadingQuotes/index.ts index d26f49367..3b9896b58 100644 --- a/src/plugins/loadingQuotes/index.ts +++ b/src/plugins/loadingQuotes/index.ts @@ -66,11 +66,11 @@ export default definePlugin({ find: "#{intl::LOADING_DID_YOU_KNOW}", replacement: [ { - match: /"_loadingText".+?(?=(\i)\[.{0,10}\.random)/, + match: /_loadingText.+?(?=(\i)\[.{0,10}\.random)/, replace: "$&$self.mutateQuotes($1)," }, { - match: /"_eventLoadingText".+?(?=(\i)\[.{0,10}\.random)/, + match: /_eventLoadingText.+?(?=(\i)\[.{0,10}\.random)/, replace: "$&$self.mutateQuotes($1),", predicate: () => settings.store.replaceEvents } diff --git a/src/plugins/mentionAvatars/index.tsx b/src/plugins/mentionAvatars/index.tsx index 3e8e7c9ee..741116c76 100644 --- a/src/plugins/mentionAvatars/index.tsx +++ b/src/plugins/mentionAvatars/index.tsx @@ -49,10 +49,16 @@ export default definePlugin({ patches: [{ find: ".USER_MENTION)", - replacement: { - match: /children:"@"\.concat\((null!=\i\?\i:\i)\)(?<=\.useName\((\i)\).+?)/, - replace: "children:$self.renderUsername({username:$1,user:$2})" - } + replacement: [ + { + match: /children:"@"\.concat\((null!=\i\?\i:\i)\)(?<=\.useName\((\i)\).+?)/, + replace: "children:$self.renderUsername({username:$1,user:$2})" + }, + { + match: /children:`@\$\{(\i\?\?\i)\}`(?<=\.useName\((\i)\).+?)/, + replace: "children:$self.renderUsername({username:$1,user:$2})" + } + ] }, { find: ".ROLE_MENTION)", diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 4d765e9e6..ef32ad514 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -408,7 +408,7 @@ export default definePlugin({ { // Updated message transformer(?) - find: "THREAD_STARTER_MESSAGE?null==", + find: ".PREMIUM_REFERRAL&&(", replacement: [ { // Pass through editHistory & deleted & original attachments to the "edited message" transformer diff --git a/src/plugins/normalizeMessageLinks/index.ts b/src/plugins/normalizeMessageLinks/index.ts deleted file mode 100644 index 82076ef20..000000000 --- a/src/plugins/normalizeMessageLinks/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NormalizeMessageLinks", - description: "Strip canary/ptb from message links", - authors: [Devs.bb010g], - patches: [ - { - find: "#{intl::COPY_MESSAGE_LINK}", - replacement: { - match: /\.concat\(location\.host\)/, - replace: ".concat($self.normalizeHost(location.host))", - }, - }, - ], - normalizeHost(host: string) { - return host.replace(/(^|\b)(canary\.|ptb\.)(discord.com)$/, "$1$3"); - }, -}); diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index b3aaa528e..71b863eb1 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -72,7 +72,7 @@ export default definePlugin({ patches: [ { - find: '"private-channels-".concat(', + find: '"dm-quick-launcher"===', replacement: [ { // Filter out pinned channels from the private channel list @@ -87,15 +87,15 @@ export default definePlugin({ // Rendering { - match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.\i),\{channel:.+?)/, + match: /renderRow(?:",|=)(\i)=>{(?<=renderDM(?:",|=).+?(\i\.\i),\{channel:.+?)/, replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2)();" }, { - match: /"renderSection",(\i)=>{/, + match: /renderSection(?:",|=)(\i)=>{/, replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);" }, { - match: /"renderSection".{0,300}?"span",{/, + match: /renderSection(?:",|=).{0,300}?"span",{/, replace: "$&...$self.makeSpanProps()," }, @@ -105,7 +105,7 @@ export default definePlugin({ replace: "$1($2-$self.categoryLen())" }, { - match: /"getRowHeight",\((\i),(\i)\)=>{/, + match: /getRowHeight(?:",|=)\((\i),(\i)\)=>{/, replace: "$&if($self.isChannelHidden($1,$2))return 0;" }, diff --git a/src/plugins/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx index 8c6ba66d8..ea33b3c6a 100644 --- a/src/plugins/platformIndicators/index.tsx +++ b/src/plugins/platformIndicators/index.tsx @@ -42,7 +42,7 @@ export interface Session { const SessionsStore = findStoreLazy("SessionsStore") as { getSessions(): Record; }; -const { useStatusFillColor } = mapMangledModuleLazy(".concat(.5625*", { +const { useStatusFillColor } = mapMangledModuleLazy([".5625*", "translate"], { useStatusFillColor: filters.byCode(".hex") }); @@ -236,8 +236,8 @@ export default definePlugin({ }, { // Fix sizes for mobile indicators which aren't online - match: /(?<=\(\i\.status,)(\i)(?=,(\i),\i\))/, - replace: (_, userStatus, isMobile) => `${isMobile}?"online":${userStatus}` + match: /(?<=\(\i\.status,)(\i)(?=,\{.{0,15}isMobile:(\i))/, + replace: '$2?"online":$1' }, { // Make isMobile true no matter the status diff --git a/src/plugins/revealAllSpoilers/index.ts b/src/plugins/revealAllSpoilers/index.ts index 814abb202..173632d7a 100644 --- a/src/plugins/revealAllSpoilers/index.ts +++ b/src/plugins/revealAllSpoilers/index.ts @@ -32,7 +32,7 @@ export default definePlugin({ { find: ".removeObscurity,", replacement: { - match: /(?<="removeObscurity",(\i)=>{)/, + match: /(?<=removeObscurity(?:",|=)(\i)=>{)/, replace: (_, event) => `$self.reveal(${event});` } } diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index 7de4044a7..675bfe614 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -31,7 +31,7 @@ const Transforms = findByPropsLazy("insertNodes", "textToText"); const Editor = findByPropsLazy("start", "end", "toSlateRange"); const ChatInputTypes = findByPropsLazy("FORM", "USER_PROFILE"); const InputComponent = findComponentByCodeLazy("editorClassName", "CHANNEL_TEXT_AREA"); -const createChannelRecordFromServer = findByCodeLazy(".GUILD_TEXT])", "fromServer)"); +const createChannelRecordFromServer = findByCodeLazy(".GUILD_TEXT]", "fromServer)"); interface UserProps { discordId: string; diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index 872efdc64..eb03942f5 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -92,7 +92,7 @@ export default definePlugin({ // Slate { // Same find as FullUserInChatbox - find: ':"text":', + find: '"text":"locked"', replacement: [ { match: /let\{id:(\i),guildId:\i,channelId:(\i)[^}]*\}.*?\.\i,{(?=children)/, diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 484352a13..f1b08c654 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -147,7 +147,7 @@ export default definePlugin({ all: true, // Render null instead of the buttons if the channel is hidden replacement: { - match: /(?<="renderOpenChatButton",\(\)=>{)/, + match: /(?<=renderOpenChatButton(?:",|=)\(\)=>{)/, replace: "if($self.isHiddenChannel(this.props.channel))return null;" } }, @@ -200,7 +200,7 @@ export default definePlugin({ }, { // Hide the new version of unreads box for hidden channels - find: '"ChannelListUnreadsStore",', + find: '"ChannelListUnreadsStore"', replacement: { match: /(?<=\.id\)\))(?=&&\(0,\i\.\i\)\((\i)\))/, replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` @@ -227,19 +227,19 @@ export default definePlugin({ find: "Missing channel in Channel.renderHeaderToolbar", replacement: [ { - match: /"renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, + match: /renderHeaderToolbar(?:",|=)\(\)=>{.+?case \i\.\i\.GUILD_TEXT:(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` }, { - match: /"renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, + match: /renderHeaderToolbar(?:",|=)\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` }, { - match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/, + match: /renderMobileToolbar(?:",|=)\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/, replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;` }, { - match: /(?<="renderHeaderBar",\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/, + match: /(?<=renderHeaderBar(?:",|=)\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/, replace: (_, channel) => `||$self.isHiddenChannel(${channel})` }, { @@ -335,6 +335,13 @@ export default definePlugin({ match: /(?<="\+"\.concat\()\i/, replace: overflowTextAmount => "" + `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&(${overflowTextAmount}-1)<=0?"":${overflowTextAmount}` + }, + { + // Show only the plus text without overflowed children amount + // if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen + match: /(?<=`\+\$\{)\i(?=\})/, + replace: overflowTextAmount => "" + + `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&(${overflowTextAmount}-1)<=0?"":${overflowTextAmount}` } ] }, diff --git a/src/plugins/showHiddenThings/index.ts b/src/plugins/showHiddenThings/index.ts index b788a8bd1..9a329474d 100644 --- a/src/plugins/showHiddenThings/index.ts +++ b/src/plugins/showHiddenThings/index.ts @@ -52,7 +52,7 @@ export default definePlugin({ }, }, { - find: "INVITES_DISABLED))||", + find: "INVITES_DISABLED)||", predicate: () => settings.store.showInvitesPaused, replacement: { match: /\i\.\i\.can\(\i\.\i.MANAGE_GUILD,\i\)/, diff --git a/src/plugins/spotifyCrack/index.ts b/src/plugins/spotifyCrack/index.ts index 37504be2e..505e53a12 100644 --- a/src/plugins/spotifyCrack/index.ts +++ b/src/plugins/spotifyCrack/index.ts @@ -51,7 +51,7 @@ export default definePlugin({ }, }, { - find: '"displayName","SpotifyStore")', + find: "}getPlayableComputerDevices(){", replacement: [ { predicate: () => settings.store.noSpotifyAutoPause, diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts index 9cb95874c..25753b39d 100644 --- a/src/plugins/themeAttributes/index.ts +++ b/src/plugins/themeAttributes/index.ts @@ -40,10 +40,16 @@ export default definePlugin({ // popout profiles { find: "#{intl::LABEL_WITH_ONLINE_STATUS}", - replacement: { - match: /src:null!=\i\?(\i).{1,50}"aria-hidden":!0/, - replace: "$&,style:$self.getAvatarStyles($1)" - } + replacement: [ + { + match: /src:null!=\i\?(\i).{1,50}"aria-hidden":!0/, + replace: "$&,style:$self.getAvatarStyles($1)" + }, + { + match: /src:(\i)\?\?void 0.{1,50}"aria-hidden":!0/, + replace: "$&,style:$self.getAvatarStyles($1)" + } + ] }, // chat avatars { diff --git a/src/plugins/typingTweaks/index.tsx b/src/plugins/typingTweaks/index.tsx index 8369497c2..8b558562b 100644 --- a/src/plugins/typingTweaks/index.tsx +++ b/src/plugins/typingTweaks/index.tsx @@ -111,7 +111,7 @@ export default definePlugin({ replace: "$self.renderTypingUsers({ users: arguments[0]?.typingUserObjects, guildId: arguments[0]?.channel?.guild_id, children: $& })" }, { - match: /(?<=function \i\(\i\)\{)(?=[^}]+?\{channel:\i,isThreadCreation:\i=!1\})/, + match: /(?<=function \i\(\i\)\{)(?=[^}]+?\{channel:\i,isThreadCreation:\i=!1,\.\.\.\i\})/, replace: "let typingUserObjects = $self.useTypingUsers(arguments[0]?.channel);" }, { diff --git a/src/plugins/userMessagesPronouns/index.ts b/src/plugins/userMessagesPronouns/index.ts index 9eaab235f..56721e11f 100644 --- a/src/plugins/userMessagesPronouns/index.ts +++ b/src/plugins/userMessagesPronouns/index.ts @@ -44,7 +44,7 @@ export default definePlugin({ replacement: [ { // Add next to username (compact mode) - match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\)\),(?=\i)/g, + match: /className:\i\(\)\(\i\.className(?:,\i\.\i)?,\i\)\}\)(?:\))?,(?=\i)/g, replace: "$&$self.CompactPronounsChatComponentWrapper(arguments[0]),", }, ] diff --git a/src/plugins/usrbg/index.tsx b/src/plugins/usrbg/index.tsx index 86f122c8e..36efe355e 100644 --- a/src/plugins/usrbg/index.tsx +++ b/src/plugins/usrbg/index.tsx @@ -54,9 +54,9 @@ export default definePlugin({ settings, patches: [ { - find: '.banner)==null?"COMPLETE"', + find: ':"SHOULD_LOAD");', replacement: { - match: /(?<=void 0:)\i.getPreviewBanner\(\i,\i,\i\)/, + match: /\i(?:\?)?.getPreviewBanner\(\i,\i,\i\)(?=.{0,100}"COMPLETE")/, replace: "$self.patchBannerUrl(arguments[0])||$&" } diff --git a/src/plugins/validReply/index.ts b/src/plugins/validReply/index.ts index 0f0a198dd..d5d54195e 100644 --- a/src/plugins/validReply/index.ts +++ b/src/plugins/validReply/index.ts @@ -44,10 +44,16 @@ export default definePlugin({ }, { find: "ReferencedMessageStore", - replacement: { - match: /constructor\(\)\{\i\(this,"_channelCaches",new Map\)/, - replace: "$&;$self.setReplyStore(this);" - } + replacement: [ + { + match: /constructor\(\)\{\i\(this,"_channelCaches",new Map\)/, + replace: "$&;$self.setReplyStore(this);" + }, + { + match: /_channelCaches=new Map;/, + replace: "$&_=$self.setReplyStore(this);" + } + ] } ], diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx index 891a2c7c4..bbcebfaa4 100644 --- a/src/plugins/viewIcons/index.tsx +++ b/src/plugins/viewIcons/index.tsx @@ -206,10 +206,16 @@ export default definePlugin({ // Banners { find: 'backgroundColor:"COMPLETE"', - replacement: { - match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?"url\("\.concat\(\2,)/, - replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,` - } + replacement: [ + { + match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?"url\("\.concat\(\2,)/, + replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,` + }, + { + match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?`url\(\$\{\2\}\))/, + replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,` + } + ] }, // Group DMs top small & large icon { diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index f8c77ab95..e4b9c510f 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -24,9 +24,9 @@ import { saveFile } from "@utils/web"; import { filters, mapMangledModuleLazy } from "@webpack"; import { ComponentDispatch } from "@webpack/common"; -const ctxMenuCallbacks = mapMangledModuleLazy('.tagName)==="TEXTAREA"||', { - contextMenuCallbackWeb: filters.byCode('.tagName)==="INPUT"||'), - contextMenuCallbackNative: filters.byCode('.tagName)==="TEXTAREA"||') +const ctxMenuCallbacks = mapMangledModuleLazy('closest("[contenteditable=true]")', { + contextMenuCallbackWeb: filters.byCode('"[contenteditable=true]"'), + contextMenuCallbackNative: filters.byCode('.getPropertyValue("-webkit-user-select")') }); async function fetchImage(url: string) { diff --git a/src/plugins/webKeybinds.web/index.ts b/src/plugins/webKeybinds.web/index.ts index 6a0da3573..783f965d9 100644 --- a/src/plugins/webKeybinds.web/index.ts +++ b/src/plugins/webKeybinds.web/index.ts @@ -55,7 +55,7 @@ export default definePlugin({ break; case ",": e.preventDefault(); - SettingsRouter.open("My Account"); + SettingsRouter.openUserSettings("my_account_panel"); break; default: if (e.key >= "1" && e.key <= "9") { diff --git a/src/plugins/webScreenShareFixes.web/index.ts b/src/plugins/webScreenShareFixes.web/index.ts index 624423001..8eb2e6269 100644 --- a/src/plugins/webScreenShareFixes.web/index.ts +++ b/src/plugins/webScreenShareFixes.web/index.ts @@ -21,6 +21,10 @@ export default definePlugin({ match: /"x-google-max-bitrate=".concat\(\i\)/, replace: '"x-google-max-bitrate=".concat("80_000")' }, + { + match: /`x-google-max-bitrate=\$\{\i\}`/, + replace: '"x-google-max-bitrate=80_000"' + }, { match: ";level-asymmetry-allowed=1", replace: ";b=AS:800000;level-asymmetry-allowed=1" @@ -28,7 +32,11 @@ export default definePlugin({ { match: /;usedtx=".concat\((\i)\?"0":"1"\)/, replace: '$&.concat($1?";stereo=1;sprop-stereo=1":"")' - } + }, + { + match: /;usedtx=\$\{(\i)\?"0":"1"\}/, + replace: '$&${$1?";stereo=1;sprop-stereo=1":""}' + }, ] } ] diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx index 723301164..7a39f35b2 100644 --- a/src/plugins/whoReacted/index.tsx +++ b/src/plugins/whoReacted/index.tsx @@ -99,7 +99,7 @@ export default definePlugin({ find: "cleanAutomaticAnchor(){", replacement: { - match: /constructor\(\i\)\{(?=.{0,100}automaticAnchor)/, + match: /constructor\(\i\)\{(?=.{0,100}(?:automaticAnchor|\.messages\.loadingMore))/, replace: "$&$self.setScrollObj(this);" } } diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index f44ba0343..e830e52ef 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -107,7 +107,7 @@ waitFor(m => { } const str = String(m); - return str.includes(".resolve({theme:null") && !str.includes("useMemo"); + return str.includes(".resolve({theme:") && str.includes('"refresh-fast-follow-avatars"') && !str.includes("useMemo"); }, m => useToken = m); export const MaskedLink = waitForComponent("MaskedLink", filters.componentByCode("MASKED_LINK)")); diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index c76c3ead5..368d4d108 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -143,7 +143,7 @@ export const UploadHandler = { export const ApplicationAssetUtils = mapMangledModuleLazy("getAssetImage: size must === [", { fetchAssetIds: filters.byCode('.startsWith("http:")', ".dispatch({"), - getAssetFromImageURL: filters.byCode("].serialize(", ',":"'), + getAssetFromImageURL: filters.byCode("].serialize(", ":null"), getAssetImage: filters.byCode("getAssetImage: size must === ["), getAssets: filters.byCode(".assets") }); @@ -160,7 +160,7 @@ export const ChannelRouter: t.ChannelRouter = mapMangledModuleLazy('"Thread must }); export let SettingsRouter: any; -waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m); +waitFor(["openUserSettings", "USER_SETTINGS_MODAL_KEY"], m => SettingsRouter = m); export const PermissionsBits: t.PermissionsBits = findLazy(m => typeof m.ADMINISTRATOR === "bigint"); From e40189e365b875f9ed35945bf2402a0a8f6e3352 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 5 Feb 2026 23:45:29 +0100 Subject: [PATCH 4/6] fix PinDMs & DisableCallIdle; remove some legacy code --- src/plugins/_api/commands.ts | 3 ++- src/plugins/betterGifPicker/index.ts | 4 ---- src/plugins/consoleJanitor/index.tsx | 22 ++++--------------- src/plugins/disableCallIdle/index.ts | 14 +++++++----- src/plugins/mentionAvatars/index.tsx | 14 ++++-------- .../pinDms/components/CreateCategoryModal.tsx | 4 ++-- src/plugins/pinDms/data.ts | 5 ----- src/plugins/pinDms/index.tsx | 4 ++-- src/plugins/showHiddenChannels/index.tsx | 7 ------ src/plugins/themeAttributes/index.ts | 4 ---- src/plugins/validReply/index.ts | 3 ++- src/plugins/viewIcons/index.tsx | 14 ++++-------- src/plugins/webScreenShareFixes.web/index.ts | 8 ------- 13 files changed, 28 insertions(+), 78 deletions(-) diff --git a/src/plugins/_api/commands.ts b/src/plugins/_api/commands.ts index 2af65b6f9..313daea3d 100644 --- a/src/plugins/_api/commands.ts +++ b/src/plugins/_api/commands.ts @@ -55,7 +55,8 @@ export default definePlugin({ { // ...children: p?.name match: /(?<=:(\i)\.displayDescription\}.{0,200}children:).{0,50}\.name(?=\}\))/, - replace: "$1.plugin||($&)" + replace: "$1.plugin||($&)", + noWarn: true // TODO: remove legacy compatibility code in the future }, { match: /children:(?=\i\?\?\i\?\.name)(?<=command:(\i),.+?)/, diff --git a/src/plugins/betterGifPicker/index.ts b/src/plugins/betterGifPicker/index.ts index bc39c6b5c..2562a7208 100644 --- a/src/plugins/betterGifPicker/index.ts +++ b/src/plugins/betterGifPicker/index.ts @@ -15,10 +15,6 @@ export default definePlugin({ { find: "renderHeaderContent(){", replacement: [ - { - match: /(?<="state",{resultType:)null/, - replace: '"Favorites"' - }, { match: /(?<=state={resultType:)null/, replace: '"Favorites"' diff --git a/src/plugins/consoleJanitor/index.tsx b/src/plugins/consoleJanitor/index.tsx index 067fa270f..d30d6ced3 100644 --- a/src/plugins/consoleJanitor/index.tsx +++ b/src/plugins/consoleJanitor/index.tsx @@ -148,10 +148,6 @@ export default definePlugin({ { find: "is not a valid locale.", replacement: [ - { - match: /\i\.error(?=\(""\.concat\(\i," is not a valid locale."\)\))/, - replace: "$self.Noop" - }, { match: /\i\.error(?=\(`\$\{\i\} is not a valid locale.`)/, replace: "$self.Noop" @@ -167,16 +163,10 @@ export default definePlugin({ }, { find: "RPCServer:WSS", - replacement: [ - { - match: /\i\.error\("Error: "\.concat\((\i)\.message/, - replace: '!$1.message.includes("EADDRINUSE")&&$&' - }, - { - match: /\i\.error\(`Error: \$\{(\i)\.message\}/, - replace: '!$1.message.includes("EADDRINUSE")&&$&' - } - ] + replacement: { + match: /\i\.error\(`Error: \$\{(\i)\.message\}/, + replace: '!$1.message.includes("EADDRINUSE")&&$&' + } }, { find: "Tried getting Dispatch instance before instantiated", @@ -195,10 +185,6 @@ export default definePlugin({ { find: "failed to send analytics events", replacement: [ - { - match: /console\.error\("\[analytics\] failed to send analytics events query: "\.concat\(\i\)\)/, - replace: "" - }, { match: /console\.error\(`\[analytics\] failed to send analytics events query: \$\{\i\}`\)/, replace: "" diff --git a/src/plugins/disableCallIdle/index.ts b/src/plugins/disableCallIdle/index.ts index 5a1ac84fe..216f1218c 100644 --- a/src/plugins/disableCallIdle/index.ts +++ b/src/plugins/disableCallIdle/index.ts @@ -25,18 +25,20 @@ export default definePlugin({ authors: [Devs.Nuckyz], patches: [ { - find: "#{intl::BOT_CALL_IDLE_DISCONNECT_2}", + find: "this.idleTimeout.start(", replacement: { - match: /,?(?=\i\(this,"idleTimeout",new \i\.\i\))/, - replace: ";return;" + match: /this\.idleTimeout\.(start|stop)/g, + replace: "$self.noop" } }, { find: "handleIdleUpdate(){", replacement: { - match: /(?<=_initialize\(\){)/, - replace: "return;" + match: "handleIdleUpdate(){", + replace: "handleIdleUpdate(){return;" } } - ] + ], + + noop() { } }); diff --git a/src/plugins/mentionAvatars/index.tsx b/src/plugins/mentionAvatars/index.tsx index 741116c76..4e79da5cf 100644 --- a/src/plugins/mentionAvatars/index.tsx +++ b/src/plugins/mentionAvatars/index.tsx @@ -49,16 +49,10 @@ export default definePlugin({ patches: [{ find: ".USER_MENTION)", - replacement: [ - { - match: /children:"@"\.concat\((null!=\i\?\i:\i)\)(?<=\.useName\((\i)\).+?)/, - replace: "children:$self.renderUsername({username:$1,user:$2})" - }, - { - match: /children:`@\$\{(\i\?\?\i)\}`(?<=\.useName\((\i)\).+?)/, - replace: "children:$self.renderUsername({username:$1,user:$2})" - } - ] + replacement: { + match: /children:`@\$\{(\i\?\?\i)\}`(?<=\.useName\((\i)\).+?)/, + replace: "children:$self.renderUsername({username:$1,user:$2})" + } }, { find: ".ROLE_MENTION)", diff --git a/src/plugins/pinDms/components/CreateCategoryModal.tsx b/src/plugins/pinDms/components/CreateCategoryModal.tsx index 4f2df28bd..7bd34219e 100644 --- a/src/plugins/pinDms/components/CreateCategoryModal.tsx +++ b/src/plugins/pinDms/components/CreateCategoryModal.tsx @@ -24,7 +24,7 @@ interface ColorPickerWithSwatchesProps { const ColorPickerWithSwatches = findComponentByCodeLazy('id:"color-picker"'); -export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/); +export const requireSettingsModal = extractAndLoadChunksLazy(['type:"USER_SETTINGS_MODAL_OPEN"']); const cl = classNameFactory("vc-pindms-modal-"); @@ -119,7 +119,7 @@ export function NewCategoryModal({ categoryId, modalProps, initialChannelId }: P export const openCategoryModal = (categoryId: string | null, channelId: string | null) => openModalLazy(async () => { - await requireSettingsMenu(); + await requireSettingsModal(); return modalProps => ; }); diff --git a/src/plugins/pinDms/data.ts b/src/plugins/pinDms/data.ts index 7671b54f3..be268ab72 100644 --- a/src/plugins/pinDms/data.ts +++ b/src/plugins/pinDms/data.ts @@ -16,11 +16,6 @@ export interface Category { collapsed?: boolean; } -const CATEGORY_BASE_KEY = "PinDMsCategories-"; -const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs"; -const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories"; -const OLD_CATEGORY_KEY = "BetterPinDMsCategories-"; - let forceUpdateDms: (() => void) | undefined = undefined; export let currentUserCategories: Category[] = []; diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 71b863eb1..609fbe6b5 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -16,7 +16,7 @@ import { findCssClassesLazy, findStoreLazy } from "@webpack"; import { Clickable, ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common"; import { contextMenus } from "./components/contextMenu"; -import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal"; +import { openCategoryModal, requireSettingsModal } from "./components/CreateCategoryModal"; import { DEFAULT_CHUNK_SIZE } from "./constants"; import { canMoveCategory, canMoveCategoryInDirection, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getCategoryByIndex, getSections, init, isPinned, moveCategory, removeCategory, usePinnedDms } from "./data"; @@ -173,7 +173,7 @@ export default definePlugin({ categoryLen, getSections, getAllUncollapsedChannels, - requireSettingsMenu, + requireSettingsMenu: requireSettingsModal, makeProps(instance, { sections }: { sections: number[]; }) { this._instance = instance; diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index f1b08c654..15e7f058c 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -329,13 +329,6 @@ export default definePlugin({ match: /\i>0(?=&&!\i&&!\i)/, replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})` }, - { - // Show only the plus text without overflowed children amount - // if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen - match: /(?<="\+"\.concat\()\i/, - replace: overflowTextAmount => "" + - `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&(${overflowTextAmount}-1)<=0?"":${overflowTextAmount}` - }, { // Show only the plus text without overflowed children amount // if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts index 25753b39d..bc50571e8 100644 --- a/src/plugins/themeAttributes/index.ts +++ b/src/plugins/themeAttributes/index.ts @@ -41,10 +41,6 @@ export default definePlugin({ { find: "#{intl::LABEL_WITH_ONLINE_STATUS}", replacement: [ - { - match: /src:null!=\i\?(\i).{1,50}"aria-hidden":!0/, - replace: "$&,style:$self.getAvatarStyles($1)" - }, { match: /src:(\i)\?\?void 0.{1,50}"aria-hidden":!0/, replace: "$&,style:$self.getAvatarStyles($1)" diff --git a/src/plugins/validReply/index.ts b/src/plugins/validReply/index.ts index d5d54195e..4c1477240 100644 --- a/src/plugins/validReply/index.ts +++ b/src/plugins/validReply/index.ts @@ -47,7 +47,8 @@ export default definePlugin({ replacement: [ { match: /constructor\(\)\{\i\(this,"_channelCaches",new Map\)/, - replace: "$&;$self.setReplyStore(this);" + replace: "$&;$self.setReplyStore(this);", + noWarn: true // TODO: remove legacy compatibility code in the future }, { match: /_channelCaches=new Map;/, diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx index bbcebfaa4..0e9c6b7f6 100644 --- a/src/plugins/viewIcons/index.tsx +++ b/src/plugins/viewIcons/index.tsx @@ -206,16 +206,10 @@ export default definePlugin({ // Banners { find: 'backgroundColor:"COMPLETE"', - replacement: [ - { - match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?"url\("\.concat\(\2,)/, - replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,` - }, - { - match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?`url\(\$\{\2\}\))/, - replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,` - } - ] + replacement: { + match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?`url\(\$\{\2\}\))/, + replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,` + } }, // Group DMs top small & large icon { diff --git a/src/plugins/webScreenShareFixes.web/index.ts b/src/plugins/webScreenShareFixes.web/index.ts index 8eb2e6269..e7fbc178e 100644 --- a/src/plugins/webScreenShareFixes.web/index.ts +++ b/src/plugins/webScreenShareFixes.web/index.ts @@ -17,10 +17,6 @@ export default definePlugin({ { find: "x-google-max-bitrate", replacement: [ - { - match: /"x-google-max-bitrate=".concat\(\i\)/, - replace: '"x-google-max-bitrate=".concat("80_000")' - }, { match: /`x-google-max-bitrate=\$\{\i\}`/, replace: '"x-google-max-bitrate=80_000"' @@ -29,10 +25,6 @@ export default definePlugin({ match: ";level-asymmetry-allowed=1", replace: ";b=AS:800000;level-asymmetry-allowed=1" }, - { - match: /;usedtx=".concat\((\i)\?"0":"1"\)/, - replace: '$&.concat($1?";stereo=1;sprop-stereo=1":"")' - }, { match: /;usedtx=\$\{(\i)\?"0":"1"\}/, replace: '$&${$1?";stereo=1;sprop-stereo=1":""}' From 185c2e5ed686d899d8ecb4f8c3009000802c032b Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:14:46 -0500 Subject: [PATCH 5/6] add back normalizemessagelinks --- .../normalizeMessageLinks/index.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/equicordplugins/normalizeMessageLinks/index.ts diff --git a/src/equicordplugins/normalizeMessageLinks/index.ts b/src/equicordplugins/normalizeMessageLinks/index.ts new file mode 100644 index 000000000..062f1ee44 --- /dev/null +++ b/src/equicordplugins/normalizeMessageLinks/index.ts @@ -0,0 +1,26 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NormalizeMessageLinks", + description: "Strip canary/ptb from message links", + authors: [Devs.bb010g], + patches: [ + { + find: "#{intl::COPY_MESSAGE_LINK}", + replacement: { + match: /(?<=\/\/\$\{)location\.host/, + replace: "$self.normalizeHost(location.host)", + }, + }, + ], + normalizeHost(host: string) { + return host.replace(/(^|\b)(canary\.|ptb\.)(discord.com)$/, "$1$3"); + }, +}); From 0f3d665dfea483e3d14c108ff9f8a8758e2a2284 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:55:48 -0500 Subject: [PATCH 6/6] fixes --- src/equicordplugins/customUserColors/index.tsx | 2 +- src/equicordplugins/remix/index.tsx | 2 +- src/equicordplugins/userpfp/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/equicordplugins/customUserColors/index.tsx b/src/equicordplugins/customUserColors/index.tsx index 5244197e3..db4db60e5 100644 --- a/src/equicordplugins/customUserColors/index.tsx +++ b/src/equicordplugins/customUserColors/index.tsx @@ -26,7 +26,7 @@ export let colors: Record = {}; })(); // needed for color picker to be available without opening settings (ty pindms!!) -const requireSettingsMenu = extractAndLoadChunksLazy(['"AppSkeleton"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}\}\),(?=.{0,15}createPromise.*?\}\),)/); +const requireSettingsMenu = extractAndLoadChunksLazy(['type:"USER_SETTINGS_MODAL_OPEN"']); const ColorIcon = () => { return ( };