feat(plugin): Dragify (#684)

* 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 <vendicated@riseup.net>
Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
Co-authored-by: prism <snawalt420@proton.me>
Co-authored-by: wearrrrr <contact@wearr.dev>
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 <keircn@proton.me>
Co-authored-by: V <vendicated@riseup.net>
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 <snawalt420@proton.me>
Co-authored-by: prism <69634294+imjustprism@users.noreply.github.com>
Co-authored-by: wearrrrr <contact@wearr.dev>
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 <nico@d3sox.me>
Co-authored-by: creations <creations@creations.works>
Co-authored-by: x3rt <git@x3rt.dev>
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 <zcomer4dthesecond@gmail.com>
Co-authored-by: Vũ Minh Nguyên <PiscesKaze@gmail.com>
This commit is contained in:
Jake 2026-02-05 20:47:07 +00:00 • committed by GitHub
parent d6ede6fa3c
commit 484cf5d67b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1797 additions and 4 deletions

View file

@ -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 <svg
@ -168,7 +168,7 @@ export default definePlugin({
if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || (channelId === currentChannelId && WindowStore.isFocused()) || userStatus !== settings.store.statusToUse) {
return;
}
if (settings.store.respectSilentPings && (message.flags & SILENT_PING_FLAG)) {return;}
if (settings.store.respectSilentPings && (message.flags & SILENT_PING_FLAG)) { return; }
const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned;
if ((settings.store.guilds.split(", ").includes(guildId) || settings.store.channels.split(", ").includes(channelId)) && mentioned) {
await showNotification(message, guildId);

View file

@ -0,0 +1,237 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import { findComponentByCodeLazy } from "@webpack";
import { createRoot, React, useEffect, useState, useStateFromStores, VoiceStateStore } from "@webpack/common";
export type GhostState = {
visible: boolean;
x: number;
y: number;
kind: "user" | "channel" | "guild";
title: string;
subtitle?: string;
iconUrl?: string;
symbol?: string;
badge?: string;
entityId?: string;
exiting: boolean;
};
const ScreenArrowIcon = findComponentByCodeLazy("3V5Zm16") as React.ComponentType<{
className?: string;
width?: number;
height?: number;
size?: number;
color?: string;
colorClass?: string;
}>;
const cl = classNameFactory("vc-dragify-");
let ghostRoot: ReturnType<typeof createRoot> | 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 = { ...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<GhostState, "visible" | "x" | "y">, 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(
<div className={cl("ghost-container")}>
<ErrorBoundary noop>
<DragGhost />
</ErrorBoundary>
</div>
);
}
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 (
<div
className={classes(cl("ghost"), state.exiting ? cl("ghost-exit") : "")}
style={{ transform: `translate3d(${state.x}px, ${state.y}px, 0)` }}
>
<div className={cl("card")}>
<div className={cl("icon")}>
{state.iconUrl
? <img className={cl("icon-image")} src={state.iconUrl} alt="" />
: <span className={cl("icon-text")}>{state.symbol ?? "#"}</span>
}
</div>
<div className={cl("body")}>
<div className={cl("title-row")}>
<div className={cl("title")}>{state.title}</div>
{state.kind === "user" && inVoice
? (isMuted
? <VoiceMutedIcon className={classes(cl("voice-icon"), cl("voice-icon-muted"))} />
: <VoiceStateIcon className={cl("voice-icon")} />)
: null}
{state.kind === "user" && inVoice && isStreaming
? <ScreenArrowIcon
className={classes(cl("voice-icon"), cl("voice-icon-stream"))}
size={14}
width={14}
height={14}
/>
: null}
</div>
{state.subtitle && <div className={cl("subtitle")}>{state.subtitle}</div>}
</div>
<div className={cl("badge")}>{state.badge ?? state.kind}</div>
</div>
</div>
);
};
function VoiceStateIcon({ className, size = 14 }: { className?: string; size?: number; }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill="none"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M7 2a1 1 0 0 0-1 1v18a1 1 0 1 0 2 0V3a1 1 0 0 0-1-1ZM11 6a1 1 0 1 1 2 0v12a1 1 0 1 1-2 0V6ZM1 8a1 1 0 0 1 2 0v8a1 1 0 1 1-2 0V8ZM16 5a1 1 0 1 1 2 0v14a1 1 0 1 1-2 0V5ZM22 8a1 1 0 0 0-1 1v6a1 1 0 1 0 2 0V9a1 1 0 0 0-1-1Z"
/>
</svg>
);
}
function VoiceMutedIcon({ className, size = 14 }: { className?: string; size?: number; }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill="none"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4l20-20ZM6.85 13.15a.5.5 0 0 1-.85-.36V3a1 1 0 0 1 2 0v8.8a.5.5 0 0 1-.15.35l-1 1ZM11 17.2v.8a1 1 0 1 0 2 0v-1.8a.5.5 0 0 0-.85-.35l-1 1a.5.5 0 0 0-.15.36ZM11 7.8V6a1 1 0 1 1 2 0v.8a.5.5 0 0 1-.15.35l-1 1a.5.5 0 0 1-.85-.36ZM17.15 10.85a.5.5 0 0 1 .85.36V19a1 1 0 1 1-2 0v-6.8a.5.5 0 0 1 .15-.35l1-1ZM2 7a1 1 0 0 0-1 1v8a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1ZM21 9a1 1 0 1 1 2 0v6a1 1 0 1 1-2 0V9Z"
/>
</svg>
);
}

File diff suppressed because it is too large Load diff

View file

@ -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;
}
}

View file

@ -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<T = Record<string, unknown>>(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<string>();
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<string>();
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<string[]> {
const sync = extractStrings(dataTransfer);
const asyncValues: string[] = [];
const itemPromises = Array.from(dataTransfer.items ?? [])
.filter(item => item.kind === "string")
.map(item => new Promise<void>(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<DragifyPayload>(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;
}

View file

@ -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;