diff --git a/package.json b/package.json index c75345d7..60fa7703 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.11.9", + "version": "1.12.0", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/src/Vencord.ts b/src/Vencord.ts index 63508eb0..48ecce97 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -33,7 +33,7 @@ import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab"; import { StartAt } from "@utils/types"; import { get as dsGet } from "./api/DataStore"; -import { showNotification } from "./api/Notifications"; +import { NotificationData, showNotification } from "./api/Notifications"; import { PlainSettings, Settings } from "./api/Settings"; import { patches, PMLogger, startAllPlugins } from "./plugins"; import { localStorage } from "./utils/localStorage"; @@ -86,6 +86,46 @@ async function syncSettings() { } } +let notifiedForUpdatesThisSession = false; + +async function runUpdateCheck() { + const notify = (data: NotificationData) => { + if (notifiedForUpdatesThisSession) return; + notifiedForUpdatesThisSession = true; + + setTimeout(() => showNotification({ + permanent: true, + noPersist: true, + ...data + }), 10_000); + }; + + try { + const isOutdated = await checkForUpdates(); + if (!isOutdated) return; + + if (Settings.autoUpdate) { + await update(); + if (Settings.autoUpdateNotification) { + notify({ + title: "Vencord has been updated!", + body: "Click here to restart", + onClick: relaunch + }); + } + return; + } + + notify({ + title: "A Vencord update is available!", + body: "Click here to view the update", + onClick: openUpdaterModal! + }); + } catch (err) { + UpdateLogger.error("Failed to check for updates", err); + } +} + async function init() { await onceReady; startAllPlugins(StartAt.WebpackReady); @@ -93,33 +133,8 @@ async function init() { syncSettings(); if (!IS_WEB && !IS_UPDATER_DISABLED) { - try { - const isOutdated = await checkForUpdates(); - if (!isOutdated) return; - - if (Settings.autoUpdate) { - await update(); - if (Settings.autoUpdateNotification) - setTimeout(() => showNotification({ - title: "Vencord has been updated!", - body: "Click here to restart", - permanent: true, - noPersist: true, - onClick: relaunch - }), 10_000); - return; - } - - setTimeout(() => showNotification({ - title: "A Vencord update is available!", - body: "Click here to view the update", - permanent: true, - noPersist: true, - onClick: openUpdaterModal! - }), 10_000); - } catch (err) { - UpdateLogger.error("Failed to check for updates", err); - } + runUpdateCheck(); + setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes } if (IS_DEV) { diff --git a/src/api/MemberListDecorators.tsx b/src/api/MemberListDecorators.tsx index ab5a618b..ada60776 100644 --- a/src/api/MemberListDecorators.tsx +++ b/src/api/MemberListDecorators.tsx @@ -21,25 +21,14 @@ import { Channel, User } from "discord-types/general/index.js"; import { JSX } from "react"; interface DecoratorProps { - activities: any[]; - channel: Channel; - /** - * Only for DM members - */ - channelName?: string; - /** - * Only for server members - */ - currentUser?: User; - guildId?: string; - isMobile: boolean; - isOwner?: boolean; - isTyping: boolean; - selected: boolean; - status: string; + type: "guild" | "dm"; user: User; - [key: string]: any; + /** only present when this is a DM list item */ + channel: Channel; + /** only present when this is a guild list item */ + isOwner: boolean; } + export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null; type OnlyIn = "guilds" | "dms"; @@ -53,18 +42,16 @@ export function removeMemberListDecorator(identifier: string) { decoratorsFactories.delete(identifier); } -export function __getDecorators(props: DecoratorProps): JSX.Element { - const isInGuild = !!(props.guildId); - +export function __getDecorators(props: DecoratorProps, type: "guild" | "dm"): JSX.Element { const decorators = Array.from( decoratorsFactories.entries(), ([key, { render: Decorator, onlyIn }]) => { - if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild)) + if ((onlyIn === "guilds" && type !== "guild") || (onlyIn === "dms" && type !== "dm")) return null; return ( - + ); } diff --git a/src/api/Notifications/Notifications.tsx b/src/api/Notifications/Notifications.tsx index 1350e5bd..96a3425b 100644 --- a/src/api/Notifications/Notifications.tsx +++ b/src/api/Notifications/Notifications.tsx @@ -18,7 +18,7 @@ import { Settings } from "@api/Settings"; import { Queue } from "@utils/Queue"; -import { ReactDOM } from "@webpack/common"; +import { createRoot } from "@webpack/common"; import type { ReactNode } from "react"; import type { Root } from "react-dom/client"; @@ -35,7 +35,7 @@ function getRoot() { const container = document.createElement("div"); container.id = "vc-notification-container"; document.body.append(container); - reactRoot = ReactDOM.createRoot(container); + reactRoot = createRoot(container); } return reactRoot; } diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index bb2df342..e609d564 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -18,7 +18,7 @@ import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; -import { LazyComponent } from "@utils/react"; +import { LazyComponent, LazyComponentWrapper } from "@utils/react"; import { React } from "@webpack/common"; import { ErrorCard } from "./ErrorCard"; @@ -107,9 +107,9 @@ const ErrorBoundary = LazyComponent(() => { } }; }) as - React.ComponentType> & { + LazyComponentWrapper> & { wrap(Component: React.ComponentType, errorBoundaryProps?: Omit, "wrappedProps">): React.FunctionComponent; - }; + }>; ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => ( diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx index f930a40d..55822069 100644 --- a/src/components/VencordSettings/PatchHelperTab.tsx +++ b/src/components/VencordSettings/PatchHelperTab.tsx @@ -18,12 +18,13 @@ import { CodeBlock } from "@components/CodeBlock"; import { debounce } from "@shared/debounce"; +import { copyToClipboard } from "@utils/clipboard"; import { Margins } from "@utils/margins"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import { makeCodeblock } from "@utils/text"; import { Patch, ReplaceFn } from "@utils/types"; import { search } from "@webpack"; -import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common"; +import { Button, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common"; import { SettingsTab, wrapTab } from "./shared"; @@ -381,8 +382,8 @@ function PatchHelper() { <> Code - - + + )} diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 2ca83b7f..21802b6a 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -87,7 +87,7 @@ async function runReporter() { result = Webpack[method](...args); } - if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail"); + if (result == null || (result.$$vencordGetWrappedComponent != null && result.$$vencordGetWrappedComponent() == null)) throw new Error("Webpack Find Fail"); } catch (e) { let logMessage = searchType; if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { diff --git a/src/main/utils/extensions.ts b/src/main/utils/extensions.ts index 8a211baf..1323bd37 100644 --- a/src/main/utils/extensions.ts +++ b/src/main/utils/extensions.ts @@ -67,13 +67,7 @@ export async function installExt(id: string) { try { await access(extDir, fsConstants.F_OK); } catch (err) { - const url = id === "fmkadmapgofadopljbjfkapdkoienihi" - // React Devtools v4.25 - // v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843 - // Unfortunately, Google does not serve old versions, so this is the only way - // This zip file is pinned to long commit hash so it cannot be changed remotely - ? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip" - : `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`; + const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`; const buf = await get(url, { headers: { diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index 87974e39..d46e67b2 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -46,8 +46,6 @@ const ContributorBadge: ProfileBadge = { let DonorBadges = {} as Record>>; async function loadBadges(noCache = false) { - DonorBadges = {}; - const init = {} as RequestInit; if (noCache) init.cache = "no-cache"; @@ -56,6 +54,8 @@ async function loadBadges(noCache = false) { .then(r => r.json()); } +let intervalId: any; + export default definePlugin({ name: "BadgeAPI", description: "API to add badges to users.", @@ -63,7 +63,7 @@ export default definePlugin({ required: true, patches: [ { - find: ".FULL_SIZE]:26", + find: ".MODAL]:26", replacement: { match: /(?=;return 0===(\i)\.length\?)(?<=(\i)\.useMemo.+?)/, replace: ";$1=$2.useMemo(()=>[...$self.getBadges(arguments[0].displayProfile),...$1],[$1])" @@ -89,6 +89,11 @@ export default definePlugin({ } ], + // for access from the console or other plugins + get DonorBadges() { + return DonorBadges; + }, + toolboxActions: { async "Refetch Badges"() { await loadBadges(true); @@ -104,6 +109,13 @@ export default definePlugin({ async start() { await loadBadges(); + + clearInterval(intervalId); + intervalId = setInterval(loadBadges, 1000 * 60 * 30); // 30 minutes + }, + + async stop() { + clearInterval(intervalId); }, getBadges(props: { userId: string; user?: User; guildId: string; }) { diff --git a/src/plugins/_api/memberListDecorators/index.tsx b/src/plugins/_api/memberListDecorators/index.tsx index 39c82a1e..90f09d8f 100644 --- a/src/plugins/_api/memberListDecorators/index.tsx +++ b/src/plugins/_api/memberListDecorators/index.tsx @@ -33,19 +33,16 @@ export default definePlugin({ find: ".lostPermission)", replacement: [ { - match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/, - replace: "$&vencordProps=$1," - }, { - match: /#{intl::GUILD_OWNER}(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/, - replace: "$&(typeof vencordProps=='undefined'?null:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps))," + match: /children:\[(?=.{0,300},lostPermissionTooltipText:)/, + replace: "children:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0],'guild')," } ] }, { find: "PrivateChannel.renderAvatar", replacement: { - match: /decorators:(\i\.isSystemDM\(\))\?(.+?):null/, - replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]),$1?$2:null]" + match: /decorators:(\i\.isSystemDM\(\)\?.+?:null)/, + replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0],'dm'),$1]" } } ] diff --git a/src/plugins/_api/serverList.ts b/src/plugins/_api/serverList.ts index dfd40de7..89c40796 100644 --- a/src/plugins/_api/serverList.ts +++ b/src/plugins/_api/serverList.ts @@ -32,9 +32,9 @@ export default definePlugin({ } }, { - find: "#{intl::SERVERS}),children", + find: ".setGuildsTree(", replacement: { - match: /(?<=#{intl::SERVERS}\),children:)\i\.map\(\i\)/, + match: /(?<=#{intl::SERVERS}\),gap:"xs",children:)\i\.map\(.{0,50}\.length\)/, replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)" } } diff --git a/src/plugins/accountPanelServerProfile/index.tsx b/src/plugins/accountPanelServerProfile/index.tsx index 2b212d34..ad63ba27 100644 --- a/src/plugins/accountPanelServerProfile/index.tsx +++ b/src/plugins/accountPanelServerProfile/index.tsx @@ -9,7 +9,7 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getCurrentChannel } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { findComponentByCodeLazy } from "@webpack"; import { ContextMenuApi, Menu, useEffect, useRef } from "@webpack/common"; import { User } from "discord-types/general"; @@ -19,8 +19,7 @@ interface UserProfileProps { originalRenderPopout: () => React.ReactNode; } -const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined"); -const styles = findByPropsLazy("accountProfilePopoutWrapper"); +const UserProfile = findComponentByCodeLazy(".POPOUT,user"); let openAlternatePopout = false; let accountPanelRef: React.RefObject | null> = { current: null }; @@ -77,7 +76,7 @@ export default definePlugin({ replace: "$self.useAccountPanelRef();$&" }, { - match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/, + match: /(\.AVATAR,children:.+?renderPopout:\((\i),\i\)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/, replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})` }, { @@ -121,14 +120,18 @@ export default definePlugin({ } const currentChannel = getCurrentChannel(); - if (currentChannel?.getGuildId() == null) { + if (currentChannel?.getGuildId() == null || !UserProfile.$$vencordGetWrappedComponent()) { return originalRenderPopout(); } return ( -
- -
+ ); }, { noop: true }) }); diff --git a/src/plugins/alwaysAnimate/index.ts b/src/plugins/alwaysAnimate/index.ts index 8edae08b..4064de9f 100644 --- a/src/plugins/alwaysAnimate/index.ts +++ b/src/plugins/alwaysAnimate/index.ts @@ -43,7 +43,7 @@ export default definePlugin({ // Status emojis find: "#{intl::GUILD_OWNER}),children:", replacement: { - match: /(\.CUSTOM_STATUS.+?animate:)\i/, + match: /(\.CUSTOM_STATUS.+?animateEmoji:)\i/, replace: "$1!0" } }, diff --git a/src/plugins/banger/index.ts b/src/plugins/banger/index.ts index f13fd351..eed0e1b4 100644 --- a/src/plugins/banger/index.ts +++ b/src/plugins/banger/index.ts @@ -36,7 +36,7 @@ export default definePlugin({ settings, patches: [ { - find: "#{intl::BAN_CONFIRM_TITLE}", + find: "#{intl::jeKpoq::raw}", // BAN_CONFIRM_TITLE replacement: { match: /src:\i\("?\d+"?\)/g, replace: "src:$self.source" diff --git a/src/plugins/betterFolders/FolderSideBar.tsx b/src/plugins/betterFolders/FolderSideBar.tsx index d2ffe6bb..40329122 100644 --- a/src/plugins/betterFolders/FolderSideBar.tsx +++ b/src/plugins/betterFolders/FolderSideBar.tsx @@ -42,15 +42,14 @@ export default ErrorBoundary.wrap(guildsBarProps => { const guilds = document.querySelector(guildsBarProps.className.split(" ").map(c => `.${c}`).join("")); // We need to display none if we are in fullscreen. Yes this seems horrible doing with css, but it's literally how Discord does it. - // Also display flex otherwise to fix scrolling - const barStyle = { - display: isFullscreen ? "none" : "flex", - gridArea: "betterFoldersSidebar" + // Also display flex otherwise to fix scrolling. + const sidebarStyle = { + display: isFullscreen ? "none" : "flex" } satisfies CSSProperties; if (!guilds || !settings.store.sidebarAnim) { return visible - ?
{Sidebar}
+ ?
{Sidebar}
: null; } @@ -62,9 +61,9 @@ export default ErrorBoundary.wrap(guildsBarProps => { leave={{ width: 0 }} config={{ duration: 200 }} > - {(animationStyle, show) => + {(animationStyle: any, show: any) => show && ( - + {Sidebar} ) diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index 3bcf0335..3dc812a4 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -16,14 +16,15 @@ * along with this program. If not, see . */ -import "./sidebarFix.css"; +import "./style.css"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack"; -import { FluxDispatcher, useMemo } from "@webpack/common"; +import { FluxDispatcher } from "@webpack/common"; +import { ReactNode } from "react"; import FolderSideBar from "./FolderSideBar"; @@ -50,6 +51,34 @@ function closeFolders() { FolderUtils.toggleGuildFolderExpand(id); } +// Nuckyz: Unsure if this should be a general utility or not +function filterTreeWithTargetNode(children: any, predicate: (node: any) => boolean) { + if (children == null) { + return false; + } + + if (!Array.isArray(children)) { + if (predicate(children)) { + return true; + } + + return filterTreeWithTargetNode(children.props?.children, predicate); + } + + let childIsTargetChild = false; + for (let i = 0; i < children.length; i++) { + const shouldKeep = filterTreeWithTargetNode(children[i], predicate); + if (shouldKeep) { + childIsTargetChild = true; + continue; + } + + children.splice(i--, 1); + } + + return childIsTargetChild; +} + export const settings = definePluginSettings({ sidebar: { type: OptionType.BOOLEAN, @@ -101,6 +130,10 @@ export const settings = definePluginSettings({ } }); +const IS_BETTER_FOLDERS_VAR = "typeof isBetterFolders!=='undefined'?isBetterFolders:arguments[0]?.isBetterFolders"; +const BETTER_FOLDERS_EXPANDED_IDS_VAR = "typeof betterFoldersExpandedIds!=='undefined'?betterFoldersExpandedIds:arguments[0]?.betterFoldersExpandedIds"; +const GRID_STYLE_NAME = "vc-betterFolders-sidebar-grid"; + export default definePlugin({ name: "BetterFolders", description: "Shows server folders on dedicated sidebar and adds folder related improvements", @@ -113,30 +146,41 @@ export default definePlugin({ find: '("guildsnav")', predicate: () => settings.store.sidebar, replacement: [ - // Create the isBetterFolders variable in the GuildsBar component + // Create the isBetterFolders and betterFoldersExpandedIds variables in the GuildsBar component + // Needed because we access this from a non-arrow closure so we can't use arguments[0] { match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/, - replace: "$&,isBetterFolders" + replace: "$&,isBetterFolders,betterFoldersExpandedIds" }, - // If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders + // Export the isBetterFolders and betterFoldersExpandedIds variable to the Guild List component { - match: /\[(\i)\]=(\(0,\i\.\i\).{0,40}getGuildsTree\(\).+?}\))(?=,)/, - replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0]?.isBetterFolders,betterFoldersOriginalTree,arguments[0]?.betterFoldersExpandedIds)` + match: /,{guildDiscoveryButton:\i,/g, + replace: "$&isBetterFolders:arguments[0]?.isBetterFolders,betterFoldersExpandedIds:arguments[0]?.betterFoldersExpandedIds," }, - // If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children + // Wrap the guild node (guild or folder) component in a div with display: none if it's not an expanded folder or a guild in an expanded folder + { + match: /switch\((\i)\.type\){.+?default:return null}/, + replace: `return $self.wrapGuildNodeComponent($1,()=>{$&},${IS_BETTER_FOLDERS_VAR},${BETTER_FOLDERS_EXPANDED_IDS_VAR});` + }, + // Export the isBetterFolders variable to the folder component + { + match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/, + replace: `$&isBetterFolders:${IS_BETTER_FOLDERS_VAR},` + }, + // Make the callback for returning the guild node component depend on isBetterFolders and betterFoldersExpandedIds + { + match: /switch\(\i\.type\).+?,\i,\i\.setNodeRef/, + replace: "$&,arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds" + }, + // If we are rendering the Better Folders sidebar, we filter out everything but the guilds and folders from the Guild List children { match: /lastTargetNode:\i\[\i\.length-1\].+?}\)(?::null)?\](?=}\))/, replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))" }, - // If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children + // If we are rendering the Better Folders sidebar, we filter out everything but the Guild List from the Sidebar children { - match: /unreadMentionsIndicatorBottom,.+?}\)\]/, - replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0]?.isBetterFolders))" - }, - // Export the isBetterFolders variable to the folders component - { - match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/, - replace: '$&isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,' + match: /unreadMentionsFixedFooter\].+?\]/, + replace: "$&.filter($self.makeGuildsBarSidebarFilter(!!arguments[0]?.isBetterFolders))" } ] }, @@ -161,7 +205,7 @@ export default definePlugin({ ] }, { - find: ".expandedFolderBackground,", + find: ".FOLDER_ITEM_ANIMATION_DURATION),", predicate: () => settings.store.sidebar, replacement: [ // We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it) @@ -181,27 +225,20 @@ export default definePlugin({ // If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded { predicate: () => !settings.store.keepIcons, - match: /expandedFolderBackground,.+?,(?=\i\(\(\i,\i,\i\)=>{let{key.{0,45}ul)(?<=selected:\i,expanded:(\i),.+?)/, + match: /folderGroupBackground.+?,(?=\i\(\(\i,\i,\i\)=>{let{key:.{0,70}"ul")(?<=selected:\i,expanded:(\i),.+?)/, replace: (m, isExpanded) => `${m}$self.shouldRenderContents(arguments[0],${isExpanded})?null:` }, + // Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar { - // Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always, - match: /\.isExpanded\),.{0,30}children:\[/, + match: /\.isExpanded\].{0,110}children:\[/, replace: "$&$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&" }, + // Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar { - // Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always, - match: /(?<=\.expandedFolderBackground.+?}\),)(?=\i,)/, + match: /(?<=\.folderGroupBackground.*?}\),)(?=\i,)/, replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)?null:" - }, - { - // Discord adds a slight bottom margin of 4px when it's expanded - // Which looks off when there's nothing open in the folder - predicate: () => !settings.store.keepIcons, - match: /(?=className:.{0,50}folderIcon)/, - replace: "style:arguments[0]?.isBetterFolders?{}:{marginBottom:0}," } ] }, @@ -221,8 +258,8 @@ export default definePlugin({ }, { // Add grid styles to fix aligment with other visual refresh elements - match: /(?<=className:)(\i\.base)(?=,)/, - replace: "`${$self.gridStyle} ${$1}`" + match: /(?<=className:)\i\.base(?=,)/, + replace: `"${GRID_STYLE_NAME} "+$&` } ] }, @@ -278,55 +315,60 @@ export default definePlugin({ } }, - gridStyle: "vc-betterFolders-sidebar-grid", + FolderSideBar, + closeFolders, - getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set) { - return useMemo(() => { - if (!isBetterFolders || expandedFolderIds == null) return originalTree; - const newTree = new GuildsTree(); - // Children is every folder and guild which is not in a folder, this filters out only the expanded folders - newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id)); - // Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them - newTree.nodes = Object.fromEntries( - Object.entries(originalTree.nodes) - .filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId)) - ); + wrapGuildNodeComponent(node: any, originalComponent: () => ReactNode, isBetterFolders: boolean, expandedFolderIds?: Set) { + if ( + !isBetterFolders || + node.type === "folder" && expandedFolderIds?.has(node.id) || + node.type === "guild" && expandedFolderIds?.has(node.parentId) + ) { + return originalComponent(); + } - return newTree; - }, [isBetterFolders, originalTree, expandedFolderIds]); + return ( +
+ {originalComponent()} +
+ ); }, makeGuildsBarGuildListFilter(isBetterFolders: boolean) { - return child => { - if (!isBetterFolders) return true; + return (child: any) => { + if (!isBetterFolders) { + return true; + } try { return child?.props?.["aria-label"] === getIntlMessage("SERVERS"); } catch (e) { console.error(e); + return true; } - - return true; }; }, - makeGuildsBarTreeFilter(isBetterFolders: boolean) { - return child => { - if (!isBetterFolders) return true; - - if (child?.props?.className?.includes("itemsContainer") && child.props.children != null) { - // Filter out everything but the scroller for the guild list - child.props.children = child.props.children.filter(child => child?.props?.onScroll != null); + makeGuildsBarSidebarFilter(isBetterFolders: boolean) { + return (child: any) => { + if (!isBetterFolders) { return true; } - return false; + try { + return filterTreeWithTargetNode(child, child => child?.props?.renderTreeNode != null); + } catch (e) { + console.error(e); + return true; + } }; }, shouldShowFolderIconAndBackground(isBetterFolders: boolean, expandedFolderIds?: Set) { - if (!isBetterFolders) return true; + if (!isBetterFolders) { + return true; + } switch (settings.store.showFolderIcon) { case FolderIconDisplay.Never: @@ -352,8 +394,5 @@ export default definePlugin({ if (props?.folderNode?.id === 1) return false; return !props?.isBetterFolders && isExpanded; - }, - - FolderSideBar, - closeFolders, + } }); diff --git a/src/plugins/betterFolders/sidebarFix.css b/src/plugins/betterFolders/style.css similarity index 68% rename from src/plugins/betterFolders/sidebarFix.css rename to src/plugins/betterFolders/style.css index 7a048eb7..a3c82dcb 100644 --- a/src/plugins/betterFolders/sidebarFix.css +++ b/src/plugins/betterFolders/style.css @@ -1,7 +1,11 @@ -/* These area names need to be hardcoded. Only betterFoldersSidebar is added by the plugin. */ +.vc-betterFolders-sidebar { + grid-area: betterFoldersSidebar +} +/* These area names need to be hardcoded. Only betterFoldersSidebar is added by the plugin. */ .visual-refresh .vc-betterFolders-sidebar-grid { - grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] min-content [channelsEnd] 1fr [end]; /* stylelint-disable-line value-keyword-case */ + /* stylelint-disable-next-line value-keyword-case */ + grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] min-content [channelsEnd] 1fr [end]; grid-template-areas: "titleBar titleBar titleBar titleBar" "guildsList betterFoldersSidebar notice notice" diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx index afef6390..a9bab1a7 100644 --- a/src/plugins/betterRoleContext/index.tsx +++ b/src/plugins/betterRoleContext/index.tsx @@ -7,11 +7,12 @@ import { definePluginSettings } from "@api/Settings"; import { getUserSettingLazy } from "@api/UserSettings"; import { ImageIcon } from "@components/Icons"; +import { copyToClipboard } from "@utils/clipboard"; import { Devs } from "@utils/constants"; import { getCurrentGuild, openImageModal } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { Clipboard, GuildStore, Menu, PermissionStore } from "@webpack/common"; +import { GuildStore, Menu, PermissionStore } from "@webpack/common"; const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild"); @@ -87,7 +88,7 @@ export default definePlugin({ Clipboard.copy(role.colorString!)} + action={() => copyToClipboard(role.colorString!)} icon={AppearanceIcon} /> ); diff --git a/src/plugins/betterRoleDot/index.ts b/src/plugins/betterRoleDot/index.ts index 3a8a1456..bdafe02c 100644 --- a/src/plugins/betterRoleDot/index.ts +++ b/src/plugins/betterRoleDot/index.ts @@ -18,8 +18,8 @@ import { Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; +import { copyWithToast } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; -import { Clipboard, Toasts } from "@webpack/common"; export default definePlugin({ name: "BetterRoleDot", @@ -84,15 +84,6 @@ export default definePlugin({ }, copyToClipBoard(color: string) { - Clipboard.copy(color); - Toasts.show({ - message: "Copied to Clipboard!", - type: Toasts.Type.SUCCESS, - id: Toasts.genId(), - options: { - duration: 1000, - position: Toasts.Position.BOTTOM - } - }); + copyWithToast(color); }, }); diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index 84e338ef..cbf94c2b 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -142,8 +142,7 @@ export default definePlugin({ // Thus, we sanity check webpack modules Layer(props: LayerProps) { try { - // @ts-ignore - [FocusLock.$$vencordInternal(), ComponentDispatch, Classes].forEach(e => e.test); + [FocusLock.$$vencordGetWrappedComponent(), ComponentDispatch, Classes].forEach(e => e.test); } catch { new Logger("BetterSettings").error("Failed to find some components"); return props.children; diff --git a/src/plugins/clientTheme/README.md b/src/plugins/clientTheme/README.md index 4b40148c..e2723f8b 100644 --- a/src/plugins/clientTheme/README.md +++ b/src/plugins/clientTheme/README.md @@ -1,4 +1,4 @@ -# Classic Client Theme +# Client Theme Revival of the old client theme experiment (The one that came before the sucky one that we actually got) diff --git a/src/plugins/clientTheme/clientTheme.css b/src/plugins/clientTheme/clientTheme.css index 795b5457..49cc3e15 100644 --- a/src/plugins/clientTheme/clientTheme.css +++ b/src/plugins/clientTheme/clientTheme.css @@ -19,16 +19,8 @@ border: thin solid var(--background-modifier-accent) !important; } -.vc-clientTheme-warning-text { - color: var(--text-danger); -} - -.vc-clientTheme-contrast-warning { - background-color: var(--background-primary); - padding: 0.5rem; - border-radius: .5rem; +.vc-clientTheme-buttons-container { + margin-top: 16px; display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; + gap: 4px; } diff --git a/src/plugins/clientTheme/components/Settings.tsx b/src/plugins/clientTheme/components/Settings.tsx new file mode 100644 index 00000000..f38380fa --- /dev/null +++ b/src/plugins/clientTheme/components/Settings.tsx @@ -0,0 +1,104 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { ErrorCard } from "@components/ErrorCard"; +import { Margins } from "@utils/margins"; +import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; +import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common"; + +import { settings } from ".."; +import { relativeLuminance } from "../utils/colorUtils"; +import { createOrUpdateThemeColorVars } from "../utils/styleUtils"; + +const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)"); +const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"==='); +const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore"); + +const cl = classNameFactory("vc-clientTheme-"); + +const colorPresets = [ + "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D", + "#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42", + "#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d", + "#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb", +]; + +function onPickColor(color: number) { + const hexColor = color.toString(16).padStart(6, "0"); + + settings.store.color = hexColor; + createOrUpdateThemeColorVars(hexColor); +} + +function setDiscordTheme(theme: string) { + saveClientTheme({ theme }); +} + +export function ThemeSettingsComponent() { + const currentTheme = useStateFromStores([ThemeStore], () => ThemeStore.theme); + const isLightTheme = currentTheme === "light"; + const oppositeTheme = isLightTheme ? "Dark" : "Light"; + + const nitroThemeEnabled = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset != null); + + const selectedLuminance = relativeLuminance(settings.store.color); + + let contrastWarning = false; + let fixableContrast = true; + + if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12) { + contrastWarning = true; + } + + if (selectedLuminance < 0.26 && selectedLuminance > 0.12) { + fixableContrast = false; + } + + // Light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels + if (isLightTheme && selectedLuminance > 0.65) { + contrastWarning = true; + fixableContrast = false; + } + + return ( +
+
+
+ Theme Color + Add a color to your Discord client theme +
+ +
+ {(contrastWarning || nitroThemeEnabled) && (<> + + Your theme won't look good! + + {contrastWarning && {">"} Selected color won't contrast well with text} + {nitroThemeEnabled && {">"} Nitro themes aren't supported} + +
+ {(contrastWarning && fixableContrast) && } + {(nitroThemeEnabled) && } +
+
+ )} +
+ ); +} + +export function ResetThemeColorComponent() { + return ( + + ); +} diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index 2b77d00d..984318e2 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -7,104 +7,21 @@ import "./clientTheme.css"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Devs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import { classes } from "@utils/misc"; import definePlugin, { OptionType, StartAt } from "@utils/types"; -import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common"; -const cl = classNameFactory("vc-clientTheme-"); +import { ResetThemeColorComponent, ThemeSettingsComponent } from "./components/Settings"; +import { disableClientTheme, startClientTheme } from "./utils/styleUtils"; -const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)"); - -const colorPresets = [ - "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D", - "#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42", - "#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d", - "#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb", -]; - -function onPickColor(color: number) { - const hexColor = color.toString(16).padStart(6, "0"); - - settings.store.color = hexColor; - updateColorVars(hexColor); -} - -const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"==='); - -function setTheme(theme: string) { - saveClientTheme({ theme }); -} - -const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore"); - -function ThemeSettings() { - const theme = useStateFromStores([ThemeStore], () => ThemeStore.theme); - const isLightTheme = theme === "light"; - const oppositeTheme = isLightTheme ? "dark" : "light"; - - const nitroTheme = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset); - const nitroThemeEnabled = nitroTheme !== undefined; - - const selectedLuminance = relativeLuminance(settings.store.color); - - let contrastWarning = false, fixableContrast = true; - if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12) - contrastWarning = true; - if (selectedLuminance < 0.26 && selectedLuminance > 0.12) - fixableContrast = false; - // light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels - if (isLightTheme && selectedLuminance > 0.65) { - contrastWarning = true; - fixableContrast = false; - } - - return ( -
-
-
- Theme Color - Add a color to your Discord client theme -
- -
- {(contrastWarning || nitroThemeEnabled) && (<> - -
-
- Warning, your theme won't look good: - {contrastWarning && Selected color won't contrast well with text} - {nitroThemeEnabled && Nitro themes aren't supported} -
- {(contrastWarning && fixableContrast) && } - {(nitroThemeEnabled) && } -
- )} -
- ); -} - -const settings = definePluginSettings({ +export const settings = definePluginSettings({ color: { type: OptionType.COMPONENT, default: "313338", - component: ThemeSettings + component: ThemeSettingsComponent }, resetColor: { type: OptionType.COMPONENT, - component: () => ( - - ) + component: ResetThemeColorComponent } }); @@ -115,185 +32,6 @@ export default definePlugin({ settings, startAt: StartAt.DOMContentLoaded, - async start() { - updateColorVars(settings.store.color); - - const styles = await getStyles(); - generateColorOffsets(styles); - generateLightModeFixes(styles); - }, - - stop() { - document.getElementById("clientThemeVars")?.remove(); - document.getElementById("clientThemeOffsets")?.remove(); - document.getElementById("clientThemeLightModeFixes")?.remove(); - } + start: () => startClientTheme(settings.store.color), + stop: disableClientTheme }); - -const visualRefreshVariableRegex = /(--neutral-\d{1,3}-hsl):.*?(\S*)%;/g; -const oldVariableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g; -const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g; -const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g; - -// generates variables per theme by: -// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable) -// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600) -function genThemeSpecificOffsets(variableLightness: Record, regex: RegExp | null, centerVariable: string): string { - return Object.entries(variableLightness).filter(([key]) => regex == null || key.search(regex) > -1) - .map(([key, lightness]) => { - const lightnessOffset = lightness - variableLightness[centerVariable]; - const plusOrMinus = lightnessOffset >= 0 ? "+" : "-"; - return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`; - }) - .join("\n"); -} - -function generateColorOffsets(styles) { - const oldVariableLightness = {} as Record; - const visualRefreshVariableLightness = {} as Record; - - // Get lightness values of --primary variables - for (const [, variable, lightness] of styles.matchAll(oldVariableRegex)) { - oldVariableLightness[variable] = parseFloat(lightness); - } - - for (const [, variable, lightness] of styles.matchAll(visualRefreshVariableRegex)) { - visualRefreshVariableLightness[variable] = parseFloat(lightness); - } - - createStyleSheet("clientThemeOffsets", [ - `.theme-light {\n ${genThemeSpecificOffsets(oldVariableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`, - `.theme-dark {\n ${genThemeSpecificOffsets(oldVariableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`, - `.visual-refresh.theme-light {\n ${genThemeSpecificOffsets(visualRefreshVariableLightness, null, "--neutral-2-hsl")} \n}`, - `.visual-refresh.theme-dark {\n ${genThemeSpecificOffsets(visualRefreshVariableLightness, null, "--neutral-69-hsl")} \n}`, - ].join("\n\n")); -} - -function generateLightModeFixes(styles: string) { - const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm; - // get light capturing groups that mention --white-500 - const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat(); - - const groupBackgroundRegex = /^([^{]*)\{background:var\(--white-500\)/m; - const groupBackgroundColorRegex = /^([^{]*)\{background-color:var\(--white-500\)/m; - // find all capturing groups that assign background or background-color directly to w500 - const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n"); - const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n"); - // create css to reassign them to --primary-100 - const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`; - const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`; - - const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m; - const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m; - // get all global variables used for backgrounds - const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500 - .map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[] - const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500 - // create css to reassign every var - const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`; - - createStyleSheet("clientThemeLightModeFixes", [ - reassignBackgrounds, - reassignBackgroundColors, - reassignVariables, - ].join("\n\n")); -} - -function captureOne(str, regex) { - const result = str.match(regex); - return (result === null) ? null : result[1]; -} - -function mapReject(arr, mapFunc) { - return arr.map(mapFunc).filter(Boolean); -} - -function updateColorVars(color: string) { - const { hue, saturation, lightness } = hexToHSL(color); - - let style = document.getElementById("clientThemeVars"); - if (!style) - style = createStyleSheet("clientThemeVars"); - - style.textContent = `:root { - --theme-h: ${hue}; - --theme-s: ${saturation}%; - --theme-l: ${lightness}%; - }`; -} - -function createStyleSheet(id, content = "") { - const style = document.createElement("style"); - style.setAttribute("id", id); - style.textContent = content.split("\n").map(line => line.trim()).join("\n"); - document.body.appendChild(style); - return style; -} - -// returns all of discord's native styles in a single string -async function getStyles(): Promise { - let out = ""; - const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); - for (const styleLinkNode of styleLinkNodes) { - const cssLink = styleLinkNode.getAttribute("href"); - if (!cssLink) continue; - - const res = await fetch(cssLink); - out += await res.text(); - } - return out; -} - -// https://css-tricks.com/converting-color-spaces-in-javascript/ -function hexToHSL(hexCode: string) { - // Hex => RGB normalized to 0-1 - const r = parseInt(hexCode.substring(0, 2), 16) / 255; - const g = parseInt(hexCode.substring(2, 4), 16) / 255; - const b = parseInt(hexCode.substring(4, 6), 16) / 255; - - // RGB => HSL - const cMax = Math.max(r, g, b); - const cMin = Math.min(r, g, b); - const delta = cMax - cMin; - - let hue: number, saturation: number, lightness: number; - - lightness = (cMax + cMin) / 2; - - if (delta === 0) { - // If r=g=b then the only thing that matters is lightness - hue = 0; - saturation = 0; - } else { - // Magic - saturation = delta / (1 - Math.abs(2 * lightness - 1)); - - if (cMax === r) - hue = ((g - b) / delta) % 6; - else if (cMax === g) - hue = (b - r) / delta + 2; - else - hue = (r - g) / delta + 4; - hue *= 60; - if (hue < 0) - hue += 360; - } - - // Move saturation and lightness from 0-1 to 0-100 - saturation *= 100; - lightness *= 100; - - return { hue, saturation, lightness }; -} - -// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance -function relativeLuminance(hexCode: string) { - const normalize = (x: number) => - x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; - - const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255); - const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255); - const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255); - - return r * 0.2126 + g * 0.7152 + b * 0.0722; -} diff --git a/src/plugins/clientTheme/utils/colorUtils.ts b/src/plugins/clientTheme/utils/colorUtils.ts new file mode 100644 index 00000000..88d94714 --- /dev/null +++ b/src/plugins/clientTheme/utils/colorUtils.ts @@ -0,0 +1,65 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// https://css-tricks.com/converting-color-spaces-in-javascript/ +export function hexToHSL(hexCode: string) { + // Hex => RGB normalized to 0-1 + const r = parseInt(hexCode.substring(0, 2), 16) / 255; + const g = parseInt(hexCode.substring(2, 4), 16) / 255; + const b = parseInt(hexCode.substring(4, 6), 16) / 255; + + // RGB => HSL + const cMax = Math.max(r, g, b); + const cMin = Math.min(r, g, b); + const delta = cMax - cMin; + + let hue: number; + let saturation: number; + let lightness: number; + + lightness = (cMax + cMin) / 2; + + if (delta === 0) { + // If r=g=b then the only thing that matters is lightness + hue = 0; + saturation = 0; + } else { + // Magic + saturation = delta / (1 - Math.abs(2 * lightness - 1)); + + if (cMax === r) { + hue = ((g - b) / delta) % 6; + } else if (cMax === g) { + hue = (b - r) / delta + 2; + } else { + hue = (r - g) / delta + 4; + } + + hue *= 60; + if (hue < 0) { + hue += 360; + } + } + + // Move saturation and lightness from 0-1 to 0-100 + saturation *= 100; + lightness *= 100; + + return { hue, saturation, lightness }; +} + +// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance +export function relativeLuminance(hexCode: string) { + const normalize = (x: number) => ( + x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4 + ); + + const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255); + const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255); + const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255); + + return r * 0.2126 + g * 0.7152 + b * 0.0722; +} diff --git a/src/plugins/clientTheme/utils/styleUtils.ts b/src/plugins/clientTheme/utils/styleUtils.ts new file mode 100644 index 00000000..bc6169d4 --- /dev/null +++ b/src/plugins/clientTheme/utils/styleUtils.ts @@ -0,0 +1,90 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { hexToHSL } from "./colorUtils"; + +const VARS_STYLE_ID = "vc-clientTheme-vars"; +const OVERRIDES_STYLE_ID = "vc-clientTheme-overrides"; + +export function createOrUpdateThemeColorVars(color: string) { + const { hue, saturation, lightness } = hexToHSL(color); + + createOrUpdateStyle(VARS_STYLE_ID, `:root { + --theme-h: ${hue}; + --theme-s: ${saturation}%; + --theme-l: ${lightness}%; + }`); +} + +export async function startClientTheme(color: string) { + createOrUpdateThemeColorVars(color); + createColorsOverrides(await getDiscordStyles()); +} + +export function disableClientTheme() { + document.getElementById(VARS_STYLE_ID)?.remove(); + document.getElementById(OVERRIDES_STYLE_ID)?.remove(); +} + +function getOrCreateStyle(styleId: string) { + const existingStyle = document.getElementById(styleId); + if (existingStyle) { + return existingStyle as HTMLStyleElement; + } + + const newStyle = document.createElement("style"); + newStyle.id = styleId; + + return document.head.appendChild(newStyle); +} + +function createOrUpdateStyle(styleId: string, css: string) { + const style = getOrCreateStyle(styleId); + style.textContent = css; +} + +/** + * @returns A string containing all the CSS styles from the Discord client. + */ +async function getDiscordStyles(): Promise { + const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); + + const cssTexts = await Promise.all(Array.from(styleLinkNodes, async node => { + if (!node.href) + return null; + + return fetch(node.href).then(res => res.text()); + })); + + return cssTexts.filter(Boolean).join("\n"); +} + +const VISUAL_REFRESH_COLORS_VARIABLES_REGEX = /(--neutral-\d{1,3}?-hsl):.+?([\d.]+?)%;/g; + +function createColorsOverrides(styles: string) { + const visualRefreshColorsLightness = {} as Record; + + for (const [, colorVariableName, lightness] of styles.matchAll(VISUAL_REFRESH_COLORS_VARIABLES_REGEX)) { + visualRefreshColorsLightness[colorVariableName] = parseFloat(lightness); + } + + const lightThemeBaseLightness = visualRefreshColorsLightness["--neutral-2-hsl"]; + const darkThemeBaseLightness = visualRefreshColorsLightness["--neutral-69-hsl"]; + + createOrUpdateStyle(OVERRIDES_STYLE_ID, [ + `.visual-refresh.theme-light {\n ${generateNewColorVars(visualRefreshColorsLightness, lightThemeBaseLightness)} \n}`, + `.visual-refresh.theme-dark {\n ${generateNewColorVars(visualRefreshColorsLightness, darkThemeBaseLightness)} \n}`, + ].join("\n\n")); +} + +function generateNewColorVars(colorsLightess: Record, baseLightness: number) { + return Object.entries(colorsLightess).map(([colorVariableName, lightness]) => { + const lightnessOffset = lightness - baseLightness; + const plusOrMinus = lightnessOffset >= 0 ? "+" : "-"; + + return `${colorVariableName}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`; + }).join("\n"); +} diff --git a/src/plugins/consoleJanitor/index.tsx b/src/plugins/consoleJanitor/index.tsx index d32f525e..7ef7ec9c 100644 --- a/src/plugins/consoleJanitor/index.tsx +++ b/src/plugins/consoleJanitor/index.tsx @@ -197,7 +197,7 @@ export default definePlugin({ }, // Patches Discord generic logger function { - find: "Σ:", + find: '"file-only"!==', predicate: () => settings.store.disableLoggers, replacement: { match: /(?<=&&)(?=console)/, diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index 5afdbdd9..e8150f8f 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -20,6 +20,7 @@ import { Devs } from "@utils/constants"; import { getCurrentChannel, getCurrentGuild } from "@utils/discord"; import { runtimeHashMessageKey } from "@utils/intlHash"; import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy"; +import { sleep } from "@utils/misc"; import { ModalAPI } from "@utils/modal"; import { relaunch } from "@utils/native"; import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches"; @@ -135,7 +136,7 @@ function makeShortcuts() { }); } - const root = Common.ReactDOM.createRoot(doc.body.appendChild(document.createElement("div"))); + const root = Common.createRoot(doc.body.appendChild(document.createElement("div"))); root.render(Common.React.createElement(component, props)); doc.addEventListener("close", () => root.unmount(), { once: true }); @@ -173,8 +174,8 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) { function unwrapProxy(value: any) { if (value[SYM_LAZY_GET]) { forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]; - } else if (value.$$vencordInternal) { - return forceLoad ? value.$$vencordInternal() : value; + } else if (value.$$vencordGetWrappedComponent) { + return forceLoad ? value.$$vencordGetWrappedComponent() : value; } return value; @@ -206,10 +207,13 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) { return value; } +const webpackModulesProbablyLoaded = Webpack.onceReady.then(() => sleep(1000)); + export default definePlugin({ name: "ConsoleShortcuts", description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.", authors: [Devs.Ven], + startAt: StartAt.Init, patches: [ { @@ -221,7 +225,7 @@ export default definePlugin({ } ], - startAt: StartAt.Init, + start() { const shortcuts = makeShortcuts(); window.shortcutList = {}; @@ -242,18 +246,16 @@ export default definePlugin({ } // unproxy loaded modules - Webpack.onceReady.then(() => { - setTimeout(() => this.eagerLoad(false), 1000); + this.eagerLoad(false); - if (!IS_WEB) { - const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative; - Native.initDevtoolsOpenEagerLoad(); - } - }); + if (!IS_WEB) { + const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative; + Native.initDevtoolsOpenEagerLoad(); + } }, async eagerLoad(forceLoad: boolean) { - await Webpack.onceReady; + await webpackModulesProbablyLoaded; const shortcuts = makeShortcuts(); diff --git a/src/plugins/copyUserURLs/index.tsx b/src/plugins/copyUserURLs/index.tsx index 7af8502d..9e15cc82 100644 --- a/src/plugins/copyUserURLs/index.tsx +++ b/src/plugins/copyUserURLs/index.tsx @@ -18,9 +18,10 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { LinkIcon } from "@components/Icons"; +import { copyToClipboard } from "@utils/clipboard"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { Clipboard, Menu } from "@webpack/common"; +import { Menu } from "@webpack/common"; import type { Channel, User } from "discord-types/general"; interface UserContextProps { @@ -36,7 +37,7 @@ const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: U Clipboard.copy(``)} + action={() => copyToClipboard(``)} icon={LinkIcon} /> ); diff --git a/src/plugins/decor/ui/components/DecorationContextMenu.tsx b/src/plugins/decor/ui/components/DecorationContextMenu.tsx index 7c1542f6..db3f0090 100644 --- a/src/plugins/decor/ui/components/DecorationContextMenu.tsx +++ b/src/plugins/decor/ui/components/DecorationContextMenu.tsx @@ -5,7 +5,8 @@ */ import { CopyIcon, DeleteIcon } from "@components/Icons"; -import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "@webpack/common"; +import { copyToClipboard } from "@utils/clipboard"; +import { Alerts, ContextMenuApi, Menu, UserStore } from "@webpack/common"; import { Decoration } from "../../lib/api"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; @@ -23,7 +24,7 @@ export default function DecorationContextMenu({ decoration }: { decoration: Deco id={cl("decoration-context-menu-copy-hash")} label="Copy Decoration Hash" icon={CopyIcon} - action={() => Clipboard.copy(decoration.hash)} + action={() => copyToClipboard(decoration.hash)} /> {decoration.authorId === UserStore.getCurrentUser().id && m?.definition?.id === "2024-09_bug_reporter"); +const isMacOS = navigator.platform.includes("Mac"); +const modKey = isMacOS ? "cmd" : "ctrl"; +const altKey = isMacOS ? "opt" : "alt"; + const settings = definePluginSettings({ toolbarDevMenu: { type: OptionType.BOOLEAN, @@ -48,7 +52,7 @@ export default definePlugin({ Devs.Ven, Devs.Nickyux, Devs.BanTheNons, - Devs.Nuckyz + Devs.Nuckyz, ], settings, @@ -75,9 +79,9 @@ export default definePlugin({ replace: "$&$self.WarningCard()," } }, - // change top right chat toolbar button from the help one to the dev one + // Change top right chat toolbar button from the help one to the dev one { - find: "toolbar:function", + find: ".CONTEXTLESS,isActivityPanelMode:", replacement: { match: /hasBugReporterAccess:(\i)/, replace: "_hasBugReporterAccess:$1=true" @@ -85,7 +89,7 @@ export default definePlugin({ predicate: () => settings.store.toolbarDevMenu }, - // makes the Favourites Server experiment allow favouriting DMs and threads + // Make the Favourites Server experiment allow favouriting DMs and threads { find: "useCanFavoriteChannel", replacement: { @@ -93,23 +97,36 @@ export default definePlugin({ replace: "false", } }, - // enable option to always record clips even if you are not streaming + // Enable option to always record clips even if you are not streaming { find: "isDecoupledGameClippingEnabled(){", replacement: { match: /\i\.isStaff\(\)/, replace: "true" } - } + }, + + // Enable experiment embed on sent experiment links + { + find: "dev://experiment/", + replacement: [ + { + match: /\i\.isStaff\(\)/, + replace: "true" + }, + // Fix some tricky experiments name causing a client crash + { + match: /.getRegisteredExperiments\(\)(?<=(\i)=.+?).+?if\(null==(\i)(?=\)return null;)/, + replace: "$&||!Object.hasOwn($1,$2)" + } + ] + }, ], start: () => !BugReporterExperiment.getCurrentConfig().hasBugReporterAccess && enableStyle(hideBugReport), stop: () => disableStyle(hideBugReport), settingsAboutComponent: () => { - const isMacOS = navigator.platform.includes("Mac"); - const modKey = isMacOS ? "cmd" : "ctrl"; - const altKey = isMacOS ? "opt" : "alt"; return ( More Information diff --git a/src/plugins/forceOwnerCrown/index.ts b/src/plugins/forceOwnerCrown/index.ts index 907d9dc0..bf115c64 100644 --- a/src/plugins/forceOwnerCrown/index.ts +++ b/src/plugins/forceOwnerCrown/index.ts @@ -29,8 +29,8 @@ export default definePlugin({ { find: "#{intl::GUILD_OWNER}),children:", replacement: { - match: /,isOwner:(\i),/, - replace: ",_isOwner:$1=$self.isGuildOwner(e)," + match: /(?<=decorators:.{0,200}?isOwner:)\i/, + replace: "$self.isGuildOwner(arguments[0])" } } ], diff --git a/src/plugins/friendsSince/index.tsx b/src/plugins/friendsSince/index.tsx index 0f4016ad..b59c8a62 100644 --- a/src/plugins/friendsSince/index.tsx +++ b/src/plugins/friendsSince/index.tsx @@ -24,7 +24,7 @@ export default definePlugin({ patches: [ // DM User Sidebar { - find: ".PANEL}),nicknameIcons", + find: ".SIDEBAR}),nicknameIcons", replacement: { match: /#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id)}\)}\)/, replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:true})" @@ -32,7 +32,15 @@ export default definePlugin({ }, // User Profile Modal { - find: "action:\"PRESS_APP_CONNECTION\"", + find: ".connections,userId:", + replacement: { + match: /#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id),.{0,100}}\)}\),/, + replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false})," + } + }, + // User Profile Modal v2 + { + find: ".MODAL_V2,onClose:", replacement: { match: /#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id),.{0,100}}\)}\),/, replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false})," diff --git a/src/plugins/imageZoom/components/Magnifier.tsx b/src/plugins/imageZoom/components/Magnifier.tsx index 009165ff..d68f916e 100644 --- a/src/plugins/imageZoom/components/Magnifier.tsx +++ b/src/plugins/imageZoom/components/Magnifier.tsx @@ -18,7 +18,7 @@ import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; -import { FluxDispatcher, useLayoutEffect, useRef, useState } from "@webpack/common"; +import { FluxDispatcher, useLayoutEffect, useMemo, useRef, useState } from "@webpack/common"; import { ELEMENT_ID } from "../constants"; import { settings } from "../index"; @@ -160,6 +160,16 @@ export const Magnifier = ErrorBoundary.wrap(({ instance, size: i } }); + const imageSrc = useMemo(() => { + try { + const imageUrl = new URL(instance.props.src); + imageUrl.searchParams.set("animated", "true"); + return imageUrl.toString(); + } catch { + return instance.props.src; + } + }, [instance.props.src]); + if (!ready) return null; const box = element.current?.getBoundingClientRect(); @@ -203,7 +213,7 @@ export const Magnifier = ErrorBoundary.wrap(({ instance, size: i }} width={`${box.width * zoom.current}px`} height={`${box.height * zoom.current}px`} - src={instance.props.src} + src={imageSrc} alt="" /> )} diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 25a0ab7d..56e1d4a2 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -21,8 +21,9 @@ import { definePluginSettings } from "@api/Settings"; import { makeRange } from "@components/PluginSettings/components"; import { debounce } from "@shared/debounce"; import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; -import { Menu, ReactDOM } from "@webpack/common"; +import { createRoot, Menu } from "@webpack/common"; import { JSX } from "react"; import type { Root } from "react-dom/client"; @@ -237,12 +238,16 @@ export default definePlugin({ }, renderMagnifier(instance) { - if (instance.props.id === ELEMENT_ID) { - if (!this.currentMagnifierElement) { - this.currentMagnifierElement = ; - this.root = ReactDOM.createRoot(this.element!); - this.root.render(this.currentMagnifierElement); + try { + if (instance.props.id === ELEMENT_ID) { + if (!this.currentMagnifierElement) { + this.currentMagnifierElement = ; + this.root = createRoot(this.element!); + this.root.render(this.currentMagnifierElement); + } } + } catch (error) { + new Logger("ImageZoom").error("Failed to render magnifier:", error); } }, diff --git a/src/plugins/ircColors/index.ts b/src/plugins/ircColors/index.ts index 50630372..d543c995 100644 --- a/src/plugins/ircColors/index.ts +++ b/src/plugins/ircColors/index.ts @@ -66,20 +66,29 @@ export default definePlugin({ { find: '="SYSTEM_TAG"', replacement: { - match: /\i.gradientClassName]\),style:/, - replace: "$&{color:$self.calculateNameColorForMessageContext(arguments[0])},_style:" + // Override colorString with our custom color and disable gradients if applying the custom color. + match: /&&null!=\i\.secondaryColor,(?<=colorString:(\i).+?(\i)=.+?)/, + replace: (m, colorString, hasGradientColors) => `${m}` + + `vcIrcColorsDummy=[${colorString},${hasGradientColors}]=$self.getMessageColorsVariables(arguments[0],${hasGradientColors}),` } }, { find: "#{intl::GUILD_OWNER}),children:", replacement: { - match: /(typingIndicatorRef:.+?},)(\i=.+?)color:null!=.{0,50}?(?=,)/, - replace: (_, rest1, rest2) => `${rest1}ircColor=$self.calculateNameColorForListContext(arguments[0]),${rest2}color:ircColor` + match: /(?<=roleName:\i,)color:/, + replace: "color:$self.calculateNameColorForListContext(arguments[0]),originalColor:" }, predicate: () => settings.store.memberListColors } ], + getMessageColorsVariables(context: any, hasGradientColors: boolean) { + const colorString = this.calculateNameColorForMessageContext(context); + const originalColorString = context?.author?.colorString; + + return [colorString, hasGradientColors && colorString === originalColorString]; + }, + calculateNameColorForMessageContext(context: any) { const userId: string | undefined = context?.message?.author?.id; const colorString = context?.author?.colorString; @@ -97,6 +106,7 @@ export default definePlugin({ ? color : colorString; }, + calculateNameColorForListContext(context: any) { const id = context?.user?.id; const colorString = context?.colorString; diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 4bce7f5e..ffe5286e 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -23,7 +23,7 @@ import { updateMessage } from "@api/MessageUpdater"; import { Settings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; +import { Devs, SUPPORT_CATEGORY_ID, VENBOT_USER_ID } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import { Logger } from "@utils/Logger"; import { classes } from "@utils/misc"; @@ -295,8 +295,8 @@ export default definePlugin({ ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) || (isEdit ? !logEdits : !logDeletes) || ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) || - // Ignore Venbot in the support channel - (message.channel_id === "1026515880080842772" && message.author?.id === "1017176847865352332"); + // Ignore Venbot in the support channels + (message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === SUPPORT_CATEGORY_ID); }, EditMarker({ message, className, children, ...props }: any) { diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 1c9ad40e..e46824b4 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import "./style.css"; + import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { isNonNullish } from "@utils/guards"; @@ -30,8 +32,8 @@ const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel"); const UserUtils = findByPropsLazy("getGlobalName"); const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds"); -const ExpandableList = findComponentByCodeLazy('"PRESS_SECTION"'); -const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon"); +const MutualsListClasses = findByPropsLazy("row", "icon", "name", "nick"); +const ExpandableList = findComponentByCodeLazy('"PRESS_SECTION"', ".header"); function getGroupDMName(channel: Channel) { return channel.name || @@ -57,7 +59,7 @@ function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) { return mutualDms.map(c => ( { onClose(); SelectedChannelActionCreators.selectPrivateChannel(c.id); @@ -66,12 +68,12 @@ function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) { -
-
{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