-
{getGroupDMName(c)}
-
{c.recipients.length + 1} Members
+
+
{getGroupDMName(c)}
+
{c.recipients.length + 1} Members
));
@@ -85,8 +87,9 @@ export default definePlugin({
authors: [Devs.amia],
patches: [
+ // User Profile Modal
{
- find: ".MUTUAL_FRIENDS?(",
+ find: ".BOT_DATA_ACCESS?(",
replacement: [
{
match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
@@ -95,6 +98,36 @@ export default definePlugin({
{
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&"
+ },
+ // Discord adds spacing between each item which pushes our tab off screen.
+ // set the gap to zero to ensure ours stays on screen
+ {
+ match: /className:\i\.tabBar/,
+ replace: '$& + " vc-mutual-gdms-modal-tab-bar"'
+ }
+ ]
+ },
+ // User Profile Modal v2
+ {
+ find: ".tabBarPanel,children:",
+ replacement: [
+ {
+ match: /items:(\i),.+?(?=return\(0,\i\.jsxs?\)\("div)/,
+ replace: "$&$self.pushSection($1,arguments[0].user);"
+ },
+ {
+ match: /\.tabBarPanel,children:(?=.+?section:(\i))/,
+ replace: "$&$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):"
+ },
+ // Make the gap between each item smaller so our tab can fit.
+ {
+ match: /className:\i\.tabBar/,
+ replace: '$& + " vc-mutual-gdms-modal-v2-tab-bar"'
+ },
+ // Make the tab bar item text smaller so our tab can fit.
+ {
+ match: /(\.tabBarItem.+?variant:)"heading-md\/normal"/,
+ replace: '$1"heading-sm/normal"'
}
]
},
@@ -130,8 +163,8 @@ export default definePlugin({
sections[IS_PATCHED] = true;
sections.push({
+ text: getMutualGDMCountText(user),
section: "MUTUAL_GDMS",
- text: getMutualGDMCountText(user)
});
} catch (e) {
new Logger("MutualGroupDMs").error("Failed to push mutual group dms section:", e);
diff --git a/src/plugins/mutualGroupDMs/style.css b/src/plugins/mutualGroupDMs/style.css
new file mode 100644
index 00000000..f0ad3c60
--- /dev/null
+++ b/src/plugins/mutualGroupDMs/style.css
@@ -0,0 +1,7 @@
+.vc-mutual-gdms-modal-tab-bar {
+ gap: 0;
+}
+
+.vc-mutual-gdms-modal-v2-tab-bar {
+ gap: 12px;
+}
diff --git a/src/plugins/noUnblockToJump/index.ts b/src/plugins/noUnblockToJump/index.ts
index 04ddf2ed..cb379bf8 100644
--- a/src/plugins/noUnblockToJump/index.ts
+++ b/src/plugins/noUnblockToJump/index.ts
@@ -19,53 +19,17 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
-
export default definePlugin({
name: "NoUnblockToJump",
description: "Allows you to jump to messages of blocked users without unblocking them",
authors: [Devs.dzshn],
patches: [
{
- // Clicking on search results to jump
- find: '.id,"Search Results"',
- replacement: [
- {
- match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/,
- replace: "if(false)$1"
- },
- {
- match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}#{intl::UNIGNORE_TO_JUMP_TITLE})/,
- replace: "if(false)$1"
- },
- ]
- },
- {
- // Jump buttton in top right corner of messages
- find: "renderJumpButton()",
- replacement: [
- {
- match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/,
- replace: "if(false)$1"
- },
- {
- match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}#{intl::UNIGNORE_TO_JUMP_TITLE})/,
- replace: "if(false)$1"
- },
- ]
- },
- {
- // Clicking on replied messages to jump
- find: '("interactionUsernameProfile',
- replacement: [
- {
- match: /.\?(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/,
- replace: "false?$1"
- },
- {
- match: /.\?(.{1,10}\.show\({.{1,50}#{intl::UNIGNORE_TO_JUMP_TITLE})/,
- replace: "false?$1"
- },
- ]
+ find: "#{intl::UNIGNORE_TO_JUMP_BODY}",
+ replacement: {
+ match: /return \i\.\i\.isBlockedForMessage\(/,
+ replace: "return true;$&"
+ }
}
]
});
diff --git a/src/plugins/nsfwGateBypass/index.ts b/src/plugins/nsfwGateBypass/index.ts
deleted file mode 100644
index 6d0cb702..00000000
--- a/src/plugins/nsfwGateBypass/index.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2025 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
.
-*/
-
-import { Devs } from "@utils/constants";
-import definePlugin from "@utils/types";
-
-export default definePlugin({
- name: "NSFWGateBypass",
- description: "Allows you to access NSFW channels without setting/verifying your age",
- authors: [Devs.Commandtechno],
- patches: [
- {
- find: ".nsfwAllowed=null",
- replacement: [
- {
- match: /(?<=\.nsfwAllowed=)null!=.+?(?=[,;])/,
- replace: "true",
- },
- {
- match: /(?<=\.ageVerificationStatus=)null!=.+?(?=[,;])/,
- replace: "3", // VERIFIED_ADULT
- }
- ],
- }
- ],
-});
diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
index 02662fe9..ed620d7f 100644
--- a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
+++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
@@ -19,10 +19,11 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
+import { copyToClipboard } from "@utils/clipboard";
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findByCodeLazy } from "@webpack";
-import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
+import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
import { UnicodeEmoji } from "@webpack/types";
import type { Guild, Role, User } from "discord-types/general";
@@ -228,7 +229,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
id={cl("copy-role-id")}
label={getIntlMessage("COPY_ID_ROLE")}
action={() => {
- Clipboard.copy(roleId);
+ copyToClipboard(roleId);
}}
/>
@@ -269,7 +270,7 @@ function UserContextMenu({ userId }: { userId: string; }) {
id={cl("copy-user-id")}
label={getIntlMessage("COPY_ID_USER")}
action={() => {
- Clipboard.copy(userId);
+ copyToClipboard(userId);
}}
/>
diff --git a/src/plugins/plainFolderIcon/index.ts b/src/plugins/plainFolderIcon/index.ts
index bb6876b5..8eb87896 100644
--- a/src/plugins/plainFolderIcon/index.ts
+++ b/src/plugins/plainFolderIcon/index.ts
@@ -16,28 +16,27 @@
* along with this program. If not, see
.
*/
+import "./style.css";
+
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "PlainFolderIcon",
- description: "Doesn't show the small guild icons in folders",
+ description: "Dont show the small guild icons in folders",
authors: [Devs.botato],
- patches: [{
- find: ".expandedFolderIconWrapper",
- replacement: [
- // there are two elements, the first one is the plain folder icon
- // the second is the four guild preview icons
- // always show this one (the plain icons)
- {
- match: /\(\i\|\|\i\)&&(\(.{0,40}\(\i\.animated)/,
- replace: "$1",
- },
- // and never show this one (the guild preview icons)
- {
- match: /\(\i\|\|!\i\)&&(\(.{0,40}\(\i\.animated)/,
- replace: "false&&$1",
- }
- ]
- }]
+
+ patches: [
+ {
+ find: ".folderPreviewGuildIconError",
+ replacement: [
+ {
+ // Discord always renders both plain and guild icons folders and uses a css transtion to switch between them
+ match: /(?<=.folderButtonContent]:(!\i))/,
+ replace: (_, hasFolderButtonContentClass) => `,"vc-plainFolderIcon-plain":${hasFolderButtonContentClass}`
+ }
+
+ ]
+ }
+ ]
});
diff --git a/src/plugins/plainFolderIcon/style.css b/src/plugins/plainFolderIcon/style.css
new file mode 100644
index 00000000..3e2992fc
--- /dev/null
+++ b/src/plugins/plainFolderIcon/style.css
@@ -0,0 +1,10 @@
+.vc-plainFolderIcon-plain {
+ /* Without this, they are a bit laggier */
+ transition: none !important;
+
+ /* Don't show the mini guild icons */
+ transform: translateZ(0);
+
+ /* The new icons are fully transparent. Add a sane default to match the old behavior */
+ background-color: color-mix(in oklab, var(--custom-folder-color, var(--bg-brand)) 40%, transparent);
+}
diff --git a/src/plugins/reactErrorDecoder/index.ts b/src/plugins/reactErrorDecoder/index.ts
index 9e2e5dc5..45ab297a 100644
--- a/src/plugins/reactErrorDecoder/index.ts
+++ b/src/plugins/reactErrorDecoder/index.ts
@@ -20,7 +20,7 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { React } from "@webpack/common";
-let ERROR_CODES: any;
+let ERROR_CODES: Record
| undefined;
export default definePlugin({
name: "ReactErrorDecoder",
@@ -28,13 +28,12 @@ export default definePlugin({
authors: [Devs.Cyn, Devs.maisymoe],
patches: [
{
- find: '"https://reactjs.org/docs/error-decoder.html?invariant="',
+ find: "React has blocked a javascript: URL as a security precaution.",
replacement: {
- match: /(function .\(.\)){(for\(var .="https:\/\/reactjs\.org\/docs\/error-decoder\.html\?invariant="\+.,.=1;.
- `${func}{var decoded=$self.decodeError.apply(null, arguments);if(decoded)return decoded;${original}}`,
- },
- },
+ match: /"https:\/\/react.dev\/errors\/"\+\i;/,
+ replace: "$&const vcDecodedError=$self.decodeError(...arguments);if(vcDecodedError)return vcDecodedError;"
+ }
+ }
],
async start() {
@@ -56,5 +55,5 @@ export default definePlugin({
index++;
return arg;
});
- },
+ }
});
diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx
index 822ebde6..32546b9b 100644
--- a/src/plugins/reviewDB/index.tsx
+++ b/src/plugins/reviewDB/index.tsx
@@ -77,23 +77,23 @@ export default definePlugin({
patches: [
{
- find: ".BITE_SIZE,user:",
+ find: ".POPOUT,user:",
replacement: {
- match: /{profileType:\i\.\i\.BITE_SIZE,children:\[/,
+ match: /children:\[(?=[^[]+?shouldShowTooltip:)/,
replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user}),"
}
},
{
- find: ".FULL_SIZE,user:",
+ find: ".MODAL,user:",
replacement: {
- match: /{profileType:\i\.\i\.FULL_SIZE,children:\[/,
+ match: /children:\[(?=[^[]+?shouldShowTooltip:)/,
replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user}),"
}
},
{
- find: 'location:"UserProfilePanel"',
+ find: ".SIDEBAR,shouldShowTooltip:",
replacement: {
- match: /{profileType:\i\.\i\.PANEL,children:\[/,
+ match: /children:\[(?=[^[]+?shouldShowTooltip:)/,
replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user}),"
}
}
diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx
index b81a0cce..71f87b13 100644
--- a/src/plugins/roleColorEverywhere/index.tsx
+++ b/src/plugins/roleColorEverywhere/index.tsx
@@ -84,8 +84,8 @@ export default definePlugin({
find: ".USER_MENTION)",
replacement: [
{
- match: /(?<=onContextMenu:\i,color:)\i(?=,onClick)(?<=user:(\i),channel:(\i).+?)/,
- replace: "$self.getColorInt($1?.id,$2?.id)",
+ match: /(?<=onContextMenu:\i,color:)\i(?<=\.getNickname\((\i),\i,(\i).+?)/,
+ replace: "$self.getColorInt($2?.id,$1)",
}
],
predicate: () => settings.store.chatMentions
diff --git a/src/plugins/serverInfo/GuildInfoModal.tsx b/src/plugins/serverInfo/GuildInfoModal.tsx
index 9f2d3008..0f08af59 100644
--- a/src/plugins/serverInfo/GuildInfoModal.tsx
+++ b/src/plugins/serverInfo/GuildInfoModal.tsx
@@ -16,7 +16,7 @@ import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildStore,
import { Guild, User } from "discord-types/general";
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
-const FriendRow = findComponentByCodeLazy(".listName,discriminatorClass");
+const FriendRow = findComponentByCodeLazy("discriminatorClass:", ".isMobileOnline", "getAvatarURL");
const cl = classNameFactory("vc-gp-");
diff --git a/src/plugins/serverInfo/styles.css b/src/plugins/serverInfo/styles.css
index 274b7d13..42a7899c 100644
--- a/src/plugins/serverInfo/styles.css
+++ b/src/plugins/serverInfo/styles.css
@@ -50,7 +50,6 @@
border-bottom: 2px solid transparent;
color: var(--interactive-normal);
cursor: pointer;
- height: 39px;
line-height: 14px;
}
diff --git a/src/plugins/shikiCodeblocks.desktop/components/ButtonRow.tsx b/src/plugins/shikiCodeblocks.desktop/components/ButtonRow.tsx
index 6f0690d9..408de154 100644
--- a/src/plugins/shikiCodeblocks.desktop/components/ButtonRow.tsx
+++ b/src/plugins/shikiCodeblocks.desktop/components/ButtonRow.tsx
@@ -16,9 +16,6 @@
* along with this program. If not, see .
*/
-import { Clipboard } from "@webpack/common";
-import { JSX } from "react";
-
import { cl } from "../utils/misc";
import { CopyButton } from "./CopyButton";
@@ -28,20 +25,14 @@ export interface ButtonRowProps {
}
export function ButtonRow({ content, theme }: ButtonRowProps) {
- const buttons: JSX.Element[] = [];
-
- if (Clipboard.SUPPORTS_COPY) {
- buttons.push(
-
- );
- }
-
- return {buttons}
;
+ return
+
+
;
}
diff --git a/src/plugins/shikiCodeblocks.desktop/hooks/useCopyCooldown.ts b/src/plugins/shikiCodeblocks.desktop/hooks/useCopyCooldown.ts
index 414500bd..d3f35fb2 100644
--- a/src/plugins/shikiCodeblocks.desktop/hooks/useCopyCooldown.ts
+++ b/src/plugins/shikiCodeblocks.desktop/hooks/useCopyCooldown.ts
@@ -16,13 +16,14 @@
* along with this program. If not, see .
*/
-import { Clipboard, React } from "@webpack/common";
+import { copyToClipboard } from "@utils/clipboard";
+import { React } from "@webpack/common";
export function useCopyCooldown(cooldown: number) {
const [copyCooldown, setCopyCooldown] = React.useState(false);
function copy(text: string) {
- Clipboard.copy(text);
+ copyToClipboard(text);
setCopyCooldown(true);
setTimeout(() => {
diff --git a/src/plugins/shikiCodeblocks.desktop/previewExample.tsx b/src/plugins/shikiCodeblocks.desktop/previewExample.tsx
index 508153b4..db7edcf0 100644
--- a/src/plugins/shikiCodeblocks.desktop/previewExample.tsx
+++ b/src/plugins/shikiCodeblocks.desktop/previewExample.tsx
@@ -2,7 +2,7 @@
import React from "react";
const handleClick = async () =>
- console.log((await import("@webpack/common")).Clipboard.copy("\u200b"));
+ console.log((await import("@utils/clipboard")).copyToClipboard("\u200b"));
export const Example: React.FC<{
real: boolean,
diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx
index 7a38bb12..7a3dd9fb 100644
--- a/src/plugins/showHiddenChannels/index.tsx
+++ b/src/plugins/showHiddenChannels/index.tsx
@@ -325,7 +325,7 @@ export default definePlugin({
]
},
{
- find: '})},"overflow"))',
+ find: '="interactive-normal",overflowCountClassName:',
replacement: [
{
// Create a variable for the channel prop
@@ -334,18 +334,21 @@ export default definePlugin({
},
{
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
- match: /\i>0(?=&&.{0,60}renderPopout)/,
+ match: /\i>0(?=&&.{0,30}Math.min)/,
replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})`
},
{
- // Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
- match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
+ // Prevent Discord from overwriting the last children with the plus button
+ // if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
+ match: /(?<=\i\.length-)1(?=\]=.{0,60}renderPopout)(?<=(\i)=\i\.length-\i.+?)/,
replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?0:1)`
},
{
- // 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)\+1/,
- replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${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}`
}
]
},
diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx
index 1f04f1f3..ac727f69 100644
--- a/src/plugins/showMeYourName/index.tsx
+++ b/src/plugins/showMeYourName/index.tsx
@@ -48,9 +48,10 @@ export default definePlugin({
authors: [Devs.Rini, Devs.TheKodeToad],
patches: [
{
- find: '?"@":""',
+ find: '"BaseUsername"',
replacement: {
- match: /(?<=onContextMenu:\i,children:)\i\+\i/,
+ /* TODO: remove \i+\i once change makes it to stable */
+ match: /(?<=onContextMenu:\i,children:)(?:\i\+\i|\i)/,
replace: "$self.renderUsername(arguments[0])"
}
},
diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx
index 4184931f..78a69a14 100644
--- a/src/plugins/spotifyControls/PlayerComponent.tsx
+++ b/src/plugins/spotifyControls/PlayerComponent.tsx
@@ -28,6 +28,7 @@ import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc";
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
+import { SeekBar } from "./SeekBar";
import { SpotifyStore, Track } from "./SpotifyStore";
const cl = classNameFactory("vc-spotify-");
@@ -160,7 +161,7 @@ const seek = debounce((v: number) => {
SpotifyStore.seek(v);
});
-function SeekBar() {
+function SpotifySeekBar() {
const { duration } = SpotifyStore.track!;
const [storePosition, isSettingPosition, isPlaying] = useStateFromStores(
@@ -181,6 +182,12 @@ function SeekBar() {
}
}, [storePosition, isSettingPosition, isPlaying]);
+ const onChange = (v: number) => {
+ if (isSettingPosition) return;
+ setPosition(v);
+ seek(v);
+ };
+
return (
{msToHuman(position)}
-
{
- if (isSettingPosition) return;
- setPosition(v);
- seek(v);
- }}
- renderValue={msToHuman}
+ onValueChange={onChange}
+ asValueChanges={onChange}
+ onValueRender={msToHuman}
/>
-
+
);
diff --git a/src/plugins/spotifyControls/SeekBar.ts b/src/plugins/spotifyControls/SeekBar.ts
new file mode 100644
index 00000000..8d6c8a30
--- /dev/null
+++ b/src/plugins/spotifyControls/SeekBar.ts
@@ -0,0 +1,25 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { LazyComponent } from "@utils/lazyReact";
+import { Slider } from "@webpack/common";
+
+export const SeekBar = LazyComponent(() => {
+ const SliderClass = Slider.$$vencordGetWrappedComponent();
+
+ // Discord's Slider does not update `state.value` when `props.initialValue` changes if state.value is not nullish.
+ // We extend their class and override their `getDerivedStateFromProps` to update the value
+ return class SeekBar extends SliderClass {
+ static getDerivedStateFromProps(props: any, state: any) {
+ const newState = super.getDerivedStateFromProps!(props, state);
+ if (newState) {
+ newState.value = props.initialValue;
+ }
+
+ return newState;
+ }
+ };
+});
diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx
index 3d119c43..0ee41414 100644
--- a/src/plugins/userVoiceShow/index.tsx
+++ b/src/plugins/userVoiceShow/index.tsx
@@ -55,7 +55,7 @@ export default definePlugin({
settings,
patches: [
- // User Popout, Full Size Profile, Direct Messages Side Profile
+ // User Popout, User Profile Modal, Direct Messages Side Profile
{
find: "#{intl::USER_PROFILE_LOAD_ERROR}",
replacement: {
diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx
index afd9d48c..07630a00 100644
--- a/src/plugins/viewIcons/index.tsx
+++ b/src/plugins/viewIcons/index.tsx
@@ -190,7 +190,7 @@ export default definePlugin({
},
patches: [
- // Avatar component used in User DMs "User Profile" popup in the right and Profiles Modal pfp
+ // Avatar component used in User DMs "User Profile" popup in the right and User Profile Modal pfp
{
find: ".overlay:void 0,status:",
replacement: [
diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts
index 07eb4a3e..45e6fa00 100644
--- a/src/plugins/webContextMenus.web/index.ts
+++ b/src/plugins/webContextMenus.web/index.ts
@@ -17,11 +17,12 @@
*/
import { definePluginSettings } from "@api/Settings";
+import { copyToClipboard } from "@utils/clipboard";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { saveFile } from "@utils/web";
import { filters, mapMangledModuleLazy } from "@webpack";
-import { Clipboard, ComponentDispatch } from "@webpack/common";
+import { ComponentDispatch } from "@webpack/common";
const ctxMenuCallbacks = mapMangledModuleLazy('.tagName)==="TEXTAREA"||', {
contextMenuCallbackWeb: filters.byCode('.tagName)==="INPUT"||'),
@@ -114,11 +115,18 @@ export default definePlugin({
// Fix silly Discord calling the non web support copy
{
match: /\i\.\i\.copy/,
- replace: "Vencord.Webpack.Common.Clipboard.copy"
+ replace: "Vencord.Util.copyToClipboard"
}
]
},
+ {
+ find: "Copy image not supported",
+ replacement: {
+ match: /(?<=(?:canSaveImage|canCopyImage)\(\i?\)\{.{0,50})!\i\.isPlatformEmbedded/g,
+ replace: "false"
+ }
+ },
// Add back Copy & Save Image
{
find: 'id:"copy-image"',
@@ -129,7 +137,7 @@ export default definePlugin({
replace: "false"
},
{
- match: /return\s*?\[\i\.\i\.canCopyImage\(\)/,
+ match: /return\s*?\[.{0,50}?(?=\?.{0,100}?id:"copy-image")/,
replace: "return [true"
},
{
@@ -223,7 +231,7 @@ export default definePlugin({
},
{
match: /\i\.\i\.copy(?=\(\i)/,
- replace: "Vencord.Webpack.Common.Clipboard.copy"
+ replace: "Vencord.Util.copyToClipboard"
}
],
all: true,
@@ -288,7 +296,7 @@ export default definePlugin({
const selection = document.getSelection();
if (!selection) return;
- Clipboard.copy(selection.toString());
+ copyToClipboard(selection.toString());
},
cut() {
diff --git a/src/utils/clipboard.ts b/src/utils/clipboard.ts
new file mode 100644
index 00000000..c098a549
--- /dev/null
+++ b/src/utils/clipboard.ts
@@ -0,0 +1,9 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+export function copyToClipboard(text: string): Promise {
+ return IS_DISCORD_DESKTOP ? DiscordNative.clipboard.copy(text) : navigator.clipboard.writeText(text);
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index ed347fdc..70ac9d42 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -19,6 +19,7 @@
export * from "../shared/debounce";
export * from "../shared/onceDefined";
export * from "./ChangeList";
+export * from "./clipboard";
export * from "./constants";
export * from "./discord";
export * from "./guards";
diff --git a/src/utils/lazyReact.tsx b/src/utils/lazyReact.tsx
index 4896a058..0a15bf92 100644
--- a/src/utils/lazyReact.tsx
+++ b/src/utils/lazyReact.tsx
@@ -4,26 +4,28 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import { ComponentType } from "react";
+import type { ComponentType } from "react";
import { makeLazy } from "./lazy";
const NoopComponent = () => null;
+export type LazyComponentWrapper = ComponentType & { $$vencordGetWrappedComponent(): ComponentType; };
+
/**
* A lazy component. The factory method is called on first render.
* @param factory Function returning a Component
* @param attempts How many times to try to get the component before giving up
* @returns Result of factory function
*/
-export function LazyComponent(factory: () => React.ComponentType, attempts = 5) {
+export function LazyComponent(factory: () => ComponentType, attempts = 5): LazyComponentWrapper> {
const get = makeLazy(factory, attempts);
const LazyComponent = (props: T) => {
const Component = get() ?? NoopComponent;
return ;
};
- LazyComponent.$$vencordInternal = get;
+ LazyComponent.$$vencordGetWrappedComponent = get;
- return LazyComponent as ComponentType;
+ return LazyComponent;
}
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
index adca15d3..7f9f6e59 100644
--- a/src/utils/misc.ts
+++ b/src/utils/misc.ts
@@ -16,8 +16,9 @@
* along with this program. If not, see .
*/
-import { Clipboard, Toasts } from "@webpack/common";
+import { Toasts } from "@webpack/common";
+import { copyToClipboard } from "./clipboard";
import { DevsById } from "./constants";
/**
@@ -35,12 +36,8 @@ export function sleep(ms: number): Promise {
return new Promise(r => setTimeout(r, ms));
}
-export function copyWithToast(text: string, toastMessage = "Copied to clipboard!") {
- if (Clipboard.SUPPORTS_COPY) {
- Clipboard.copy(text);
- } else {
- toastMessage = "Your browser does not support copying to clipboard";
- }
+export async function copyWithToast(text: string, toastMessage = "Copied to clipboard!") {
+ await copyToClipboard(text);
Toasts.show({
message: toastMessage,
id: Toasts.genId(),
diff --git a/src/utils/modal.tsx b/src/utils/modal.tsx
index eebdb95e..17bf3987 100644
--- a/src/utils/modal.tsx
+++ b/src/utils/modal.tsx
@@ -140,7 +140,7 @@ export type MediaModalProps = {
shouldHideMediaOptions?: boolean;
};
-// modal key: "Media Viewer Modal"
+// Modal key: "Media Viewer Modal"
export const openMediaModal: (props: MediaModalProps) => void = findByCodeLazy("hasMediaOptions", "shouldHideMediaOptions");
interface ModalAPI {
diff --git a/src/utils/updater.ts b/src/utils/updater.ts
index f99c6ca1..25fe6ee8 100644
--- a/src/utils/updater.ts
+++ b/src/utils/updater.ts
@@ -39,10 +39,15 @@ async function Unwrap(p: Promise>) {
export async function checkForUpdates() {
changes = await Unwrap(VencordNative.updater.getUpdates());
- if (changes.some(c => c.hash === gitHash)) {
- isNewer = true;
- return (isOutdated = false);
+
+ // we only want to check this for the git updater, not the http updater
+ if (!IS_STANDALONE) {
+ if (changes.some(c => c.hash === gitHash)) {
+ isNewer = true;
+ return (isOutdated = false);
+ }
}
+
return (isOutdated = changes.length > 0);
}
diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx
index 8957c254..090d9898 100644
--- a/src/webpack/common/internal.tsx
+++ b/src/webpack/common/internal.tsx
@@ -16,19 +16,19 @@
* along with this program. If not, see .
*/
-import { LazyComponent } from "@utils/react";
+import { LazyComponent, LazyComponentWrapper } from "@utils/react";
// eslint-disable-next-line path-alias/no-relative
import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack";
-export function waitForComponent = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]): T {
+export function waitForComponent = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);
let myValue: T = function () {
throw new Error(`Vencord could not find the ${name} Component`);
} as any;
- const lazyComponent = LazyComponent(() => myValue) as T;
+ const lazyComponent = LazyComponent(() => myValue) as LazyComponentWrapper;
waitFor(filter, (v: any) => {
myValue = v;
Object.assign(lazyComponent, v);
diff --git a/src/webpack/common/menu.ts b/src/webpack/common/menu.ts
index 9896b3a2..5b1056dd 100644
--- a/src/webpack/common/menu.ts
+++ b/src/webpack/common/menu.ts
@@ -24,12 +24,19 @@ export const Menu = {} as t.Menu;
// Relies on .name properties added by the MenuItemDemanglerAPI
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
- // we have to do this manual require by ID because m is in this case the MenuCheckBoxItem instead of the entire module
- const module = wreq(id);
+ // We have to do this manual require by ID because m in this case is the MenuCheckBoxItem instead of the entire module
+ const exports = wreq(id);
- for (const e of Object.values(module)) {
- if (typeof e === "function" && e.name.startsWith("Menu")) {
- Menu[e.name] = e;
+ for (const exportKey in exports) {
+ // Some exports might have not been initialized yet due to circular imports, so try catch it.
+ try {
+ var exportValue = exports[exportKey];
+ } catch {
+ continue;
+ }
+
+ if (typeof exportValue === "function" && exportValue.name.startsWith("Menu")) {
+ Menu[exportValue.name] = exportValue;
}
}
});
diff --git a/src/webpack/common/react.ts b/src/webpack/common/react.ts
index 99f3f9dd..89b19506 100644
--- a/src/webpack/common/react.ts
+++ b/src/webpack/common/react.ts
@@ -17,7 +17,7 @@
*/
// eslint-disable-next-line path-alias/no-relative
-import { findByPropsLazy, waitFor } from "../webpack";
+import { findByCodeLazy, findByPropsLazy, waitFor } from "../webpack";
export let React: typeof import("react");
export let useState: typeof React.useState;
@@ -28,7 +28,9 @@ export let useRef: typeof React.useRef;
export let useReducer: typeof React.useReducer;
export let useCallback: typeof React.useCallback;
-export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render");
+export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal");
+// 299 is an error code used in createRoot and createPortal
+export const createRoot: typeof import("react-dom/client").createRoot = findByCodeLazy("(299));", ".onRecoverableError");
waitFor("useState", m => {
React = m;
diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts
index 2b8ee92a..b5f4ff5c 100644
--- a/src/webpack/common/types/components.d.ts
+++ b/src/webpack/common/types/components.d.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref } from "react";
+import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref } from "react";
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
@@ -356,7 +356,7 @@ export type SearchableSelect = ComponentType>;
-export type Slider = ComponentType typeof e === "boolean"
-});
-
export const NavigationRouter: t.NavigationRouter = mapMangledModuleLazy("Transitioning to ", {
transitionTo: filters.byCode("transitionTo -"),
transitionToGuild: filters.byCode("transitionToGuild -"),
diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts
index 9b66a5b4..4f5899bc 100644
--- a/src/webpack/patchWebpack.ts
+++ b/src/webpack/patchWebpack.ts
@@ -442,7 +442,12 @@ function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unkno
}
for (const exportKey in exports) {
- const exportValue = exports[exportKey];
+ // Some exports might have not been initialized yet due to circular imports, so try catch it.
+ try {
+ var exportValue = exports[exportKey];
+ } catch {
+ continue;
+ }
if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts
index 0e3d641b..c1847474 100644
--- a/src/webpack/webpack.ts
+++ b/src/webpack/webpack.ts
@@ -156,7 +156,14 @@ export function _blacklistBadModules(requireCache: NonNullable