diff --git a/package.json b/package.json index 0f79d21ab..776affcda 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.7.5", + "version": "1.7.8", "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 29e965fa0..655e76c3d 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -27,6 +27,7 @@ export { PlainSettings, Settings }; import "./utils/quickCss"; import "./webpack/patchWebpack"; +import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab"; import { StartAt } from "@utils/types"; import { get as dsGet } from "./api/DataStore"; @@ -85,7 +86,7 @@ async function init() { syncSettings(); - if (!IS_WEB) { + if (!IS_WEB && !IS_UPDATER_DISABLED) { try { const isOutdated = await checkForUpdates(); if (!isOutdated) return; @@ -103,16 +104,13 @@ async function init() { return; } - if (Settings.notifyAboutUpdates) - setTimeout(() => showNotification({ - title: "A Vencord update is available!", - body: "Click here to view the update", - permanent: true, - noPersist: true, - onClick() { - SettingsRouter.open("VencordUpdater"); - } - }), 10_000); + 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); } diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 0b7975300..696c12c28 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -29,7 +29,6 @@ import plugins from "~plugins"; const logger = new Logger("Settings"); export interface Settings { - notifyAboutUpdates: boolean; autoUpdate: boolean; autoUpdateNotification: boolean, useQuickCss: boolean; @@ -78,8 +77,7 @@ export interface Settings { } const DefaultSettings: Settings = { - notifyAboutUpdates: true, - autoUpdate: false, + autoUpdate: true, autoUpdateNotification: true, useQuickCss: true, themeLinks: [], diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx index 0a5d1f149..680e262d8 100644 --- a/src/components/VencordSettings/UpdaterTab.tsx +++ b/src/components/VencordSettings/UpdaterTab.tsx @@ -22,6 +22,7 @@ import { Flex } from "@components/Flex"; import { Link } from "@components/Link"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; +import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { relaunch } from "@utils/native"; import { useAwaiter } from "@utils/react"; import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater"; @@ -29,7 +30,7 @@ import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@web import gitHash from "~git-hash"; -import { SettingsTab, wrapTab } from "./shared"; +import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared"; function withDispatcher(dispatcher: React.Dispatch>, action: () => any) { return async () => { @@ -38,21 +39,24 @@ function withDispatcher(dispatcher: React.Dispatch await action(); } catch (e: any) { UpdateLogger.error("Failed to update", e); + + let err: string; if (!e) { - var err = "An unknown error occurred (error is undefined).\nPlease try again."; + err = "An unknown error occurred (error is undefined).\nPlease try again."; } else if (e.code && e.cmd) { const { code, path, cmd, stderr } = e; if (code === "ENOENT") - var err = `Command \`${path}\` not found.\nPlease install it and try again`; + err = `Command \`${path}\` not found.\nPlease install it and try again`; else { - var err = `An error occurred while running \`${cmd}\`:\n`; + err = `An error occurred while running \`${cmd}\`:\n`; err += stderr || `Code \`${code}\`. See the console for more info`; } } else { - var err = "An unknown error occurred. See the console for more info."; + err = "An unknown error occurred. See the console for more info."; } + Alerts.show({ title: "Oops!", body: ( @@ -186,7 +190,7 @@ function Newer(props: CommonProps) { } function Updater() { - const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]); + const settings = useSettings(["autoUpdate", "autoUpdateNotification"]); const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." }); @@ -203,14 +207,6 @@ function Updater() { return ( Updater Settings - settings.notifyAboutUpdates = v} - note="Shows a notification on startup" - disabled={settings.autoUpdate} - > - Get notified about new updates - settings.autoUpdate = v} @@ -253,3 +249,20 @@ function Updater() { } export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater"); + +export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () { + const UpdaterTab = wrapTab(Updater, "Updater"); + + try { + openModal(wrapTab((modalProps: ModalProps) => ( + + + + + + + ), "UpdaterModal")); + } catch { + handleSettingsTabError(); + } +}; diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css index 01cbcd557..310dba9af 100644 --- a/src/components/VencordSettings/settingsStyles.css +++ b/src/components/VencordSettings/settingsStyles.css @@ -65,3 +65,11 @@ /* discord also sets cursor: default which prevents the cursor from showing as text */ cursor: initial; } + +.vc-updater-modal { + padding: 1.5em !important; +} + +.vc-updater-modal-close-button { + float: right; +} diff --git a/src/components/VencordSettings/shared.tsx b/src/components/VencordSettings/shared.tsx index 6dd34c46f..1c5f37d82 100644 --- a/src/components/VencordSettings/shared.tsx +++ b/src/components/VencordSettings/shared.tsx @@ -42,11 +42,11 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri ); } -const onError = onlyOnce(handleComponentFailed); +export const handleSettingsTabError = onlyOnce(handleComponentFailed); -export function wrapTab(component: ComponentType, tab: string) { +export function wrapTab(component: ComponentType, tab: string) { return ErrorBoundary.wrap(component, { message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`, - onError, + onError: handleSettingsTabError, }); } diff --git a/src/plugins/_api/badges.tsx b/src/plugins/_api/badges.tsx index 16b244a19..6b1a79cd5 100644 --- a/src/plugins/_api/badges.tsx +++ b/src/plugins/_api/badges.tsx @@ -65,7 +65,7 @@ export default definePlugin({ patches: [ /* Patch the badge list component on user profiles */ { - find: "Messages.PROFILE_USER_BADGES,role:", + find: 'id:"premium",', replacement: [ { match: /&&(\i)\.push\(\{id:"premium".+?\}\);/, diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 569c3f0ac..1aea0d6ee 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -45,14 +45,14 @@ export default definePlugin({ replacement: { get match() { switch (Settings.plugins.Settings.settingsLocation) { - case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS\}/; - case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS\}/; - case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS\}/; + case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/; + case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/; + case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/; case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/; case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/; case "aboveActivity": default: - return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS\}/; + return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/; } }, replace: "...$self.makeSettingsCategories($1),$&" diff --git a/src/plugins/betterSessions/README.md b/src/plugins/betterSessions/README.md new file mode 100644 index 000000000..cf13e6c50 --- /dev/null +++ b/src/plugins/betterSessions/README.md @@ -0,0 +1,5 @@ +# BetterSessions + +Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions. + +![](https://github.com/Vendicated/Vencord/assets/9750071/4a44b617-bb8f-4dcb-93f1-b7d2575ed3d8) diff --git a/src/plugins/betterSessions/components/RenameButton.tsx b/src/plugins/betterSessions/components/RenameButton.tsx new file mode 100644 index 000000000..a0c95a6f4 --- /dev/null +++ b/src/plugins/betterSessions/components/RenameButton.tsx @@ -0,0 +1,37 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { openModal } from "@utils/modal"; +import { Button } from "@webpack/common"; + +import { SessionInfo } from "../types"; +import { RenameModal } from "./RenameModal"; + +export function RenameButton({ session, state }: { session: SessionInfo["session"], state: [string, React.Dispatch>]; }) { + return ( + + ); +} diff --git a/src/plugins/betterSessions/components/RenameModal.tsx b/src/plugins/betterSessions/components/RenameModal.tsx new file mode 100644 index 000000000..1c5783c0e --- /dev/null +++ b/src/plugins/betterSessions/components/RenameModal.tsx @@ -0,0 +1,94 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 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 { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; +import { Button, Forms, React, TextInput } from "@webpack/common"; +import { KeyboardEvent } from "react"; + +import { SessionInfo } from "../types"; +import { getDefaultName, savedSessionsCache, saveSessionsToDataStore } from "../utils"; + +export function RenameModal({ props, session, state }: { props: ModalProps, session: SessionInfo["session"], state: [string, React.Dispatch>]; }) { + const [title, setTitle] = state; + const [value, setValue] = React.useState(savedSessionsCache.get(session.id_hash)?.name ?? ""); + + function onSaveClick() { + savedSessionsCache.set(session.id_hash, { name: value, isNew: false }); + if (value !== "") { + setTitle(`${value}*`); + } else { + setTitle(getDefaultName(session.client_info)); + } + + saveSessionsToDataStore(); + props.onClose(); + } + + return ( + + + Rename + + + + New device name + ) => { + if (e.key === "Enter") { + onSaveClick(); + } + }} + /> + + + + + + + + + ); +} diff --git a/src/plugins/betterSessions/components/icons.tsx b/src/plugins/betterSessions/components/icons.tsx new file mode 100644 index 000000000..bd745e76c --- /dev/null +++ b/src/plugins/betterSessions/components/icons.tsx @@ -0,0 +1,106 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 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 { LazyComponent } from "@utils/react"; +import { findByCode } from "@webpack"; +import { SVGProps } from "react"; + +export const DiscordIcon = (props: React.PropsWithChildren>) => ( + + + +); + +export const ChromeIcon = (props: React.PropsWithChildren>) => ( + + + + + + +); + +export const EdgeIcon = (props: React.PropsWithChildren>) => ( + + + +); + +export const FirefoxIcon = (props: React.PropsWithChildren>) => ( + + + +); + +export const IEIcon = (props: React.PropsWithChildren>) => ( + + + +); + +export const OperaIcon = (props: React.PropsWithChildren>) => ( + + + +); + +export const SafariIcon = (props: React.PropsWithChildren>) => ( + + + +); + +export const UnknownIcon = (props: React.PropsWithChildren>) => ( + + + +); + +export const MobileIcon = LazyComponent(() => findByCode("M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38")); diff --git a/src/plugins/betterSessions/index.tsx b/src/plugins/betterSessions/index.tsx new file mode 100644 index 000000000..539508f80 --- /dev/null +++ b/src/plugins/betterSessions/index.tsx @@ -0,0 +1,227 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 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 { showNotification } from "@api/Notifications"; +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack"; +import { React, RestAPI, Tooltip } from "@webpack/common"; + +import { RenameButton } from "./components/RenameButton"; +import { Session, SessionInfo } from "./types"; +import { fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils"; + +const AuthSessionsStore = findStoreLazy("AuthSessionsStore"); +const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open"); + +const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer"); +const SessionIconClasses = findByPropsLazy("sessionIcon"); + +const BlobMask = findExportedComponentLazy("BlobMask"); + +const settings = definePluginSettings({ + backgroundCheck: { + type: OptionType.BOOLEAN, + description: "Check for new sessions in the background, and display notifications when they are detected", + default: false, + restartNeeded: true + }, + checkInterval: { + description: "How often to check for new sessions in the background (if enabled), in minutes", + type: OptionType.NUMBER, + default: 20, + restartNeeded: true + } +}); + +export default definePlugin({ + name: "BetterSessions", + description: "Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.", + authors: [Devs.amia], + + settings: settings, + + patches: [ + { + find: "Messages.AUTH_SESSIONS_SESSION_LOG_OUT", + replacement: [ + // Replace children with a single label with state + { + match: /({variant:"eyebrow",className:\i\.sessionInfoRow,children:).{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:\i\[\d+\]}\)\]}\)\]/, + replace: "$1$self.renderName(arguments[0])" + }, + { + match: /({variant:"text-sm\/medium",className:\i\.sessionInfoRow,children:.{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:)(\i\[\d+\])}/, + replace: "$1$self.renderTimestamp({ ...arguments[0], timeLabel: $2 })}" + }, + // Replace the icon + { + match: /\.currentSession:null\),children:\[(?<=,icon:(\i)\}.+?)/, + replace: "$& $self.renderIcon({ ...arguments[0], DeviceIcon: $1 }), false &&" + } + ] + }, + { + // Add the ability to change BlobMask's lower badge height + // (it allows changing width so we can mirror that logic) + find: "this.getBadgePositionInterpolation(", + replacement: { + match: /(\i\.animated\.rect,{id:\i,x:48-\(\i\+8\)\+4,y:)28(,width:\i\+8,height:)24,/, + replace: (_, leftPart, rightPart) => `${leftPart} 48 - ((this.props.lowerBadgeHeight ?? 16) + 8) + 4 ${rightPart} (this.props.lowerBadgeHeight ?? 16) + 8,` + } + } + ], + + renderName: ErrorBoundary.wrap(({ session }: SessionInfo) => { + const savedSession = savedSessionsCache.get(session.id_hash); + + const state = React.useState(savedSession?.name ? `${savedSession.name}*` : getDefaultName(session.client_info)); + const [title, setTitle] = state; + + // Show a "NEW" badge if the session is seen for the first time + return ( + <> + {title} + {(savedSession == null || savedSession.isNew) && ( +
+ NEW +
+ )} + + + ); + }, { noop: true }), + + renderTimestamp: ErrorBoundary.wrap(({ session, timeLabel }: { session: Session, timeLabel: string; }) => { + return ( + + {props => ( + + {timeLabel} + + )} + + ); + }, { noop: true }), + + renderIcon: ErrorBoundary.wrap(({ session, DeviceIcon }: { session: Session, DeviceIcon: React.ComponentType; }) => { + const PlatformIcon = GetPlatformIcon(session.client_info.platform); + + return ( + + + + } + lowerBadgeWidth={20} + lowerBadgeHeight={20} + > +
+ +
+
+ ); + }, { noop: true }), + + async checkNewSessions() { + const data = await RestAPI.get({ + url: "/auth/sessions" + }); + + for (const session of data.body.user_sessions) { + if (savedSessionsCache.has(session.id_hash)) continue; + + savedSessionsCache.set(session.id_hash, { name: "", isNew: true }); + showNotification({ + title: "BetterSessions", + body: `New session:\n${session.client_info.os} · ${session.client_info.platform} · ${session.client_info.location}`, + permanent: true, + onClick: () => UserSettingsModal.open("Sessions") + }); + } + + saveSessionsToDataStore(); + }, + + flux: { + USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM() { + const lastFetchedHashes: string[] = AuthSessionsStore.getSessions().map((session: SessionInfo["session"]) => session.id_hash); + + // Add new sessions to cache + lastFetchedHashes.forEach(idHash => { + if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: "", isNew: false }); + }); + + // Delete removed sessions from cache + if (lastFetchedHashes.length > 0) { + savedSessionsCache.forEach((_, idHash) => { + if (!lastFetchedHashes.includes(idHash)) savedSessionsCache.delete(idHash); + }); + } + + // Dismiss the "NEW" badge of all sessions. + // Since the only way for a session to be marked as "NEW" is going to the Devices tab, + // closing the settings means they've been viewed and are no longer considered new. + savedSessionsCache.forEach(data => { + data.isNew = false; + }); + saveSessionsToDataStore(); + } + }, + + async start() { + await fetchNamesFromDataStore(); + + this.checkNewSessions(); + if (settings.store.backgroundCheck) { + this.checkInterval = setInterval(this.checkNewSessions, settings.store.checkInterval * 60 * 1000); + } + }, + + stop() { + clearInterval(this.checkInterval); + } +}); diff --git a/src/plugins/showTimeouts/index.ts b/src/plugins/betterSessions/types.ts similarity index 59% rename from src/plugins/showTimeouts/index.ts rename to src/plugins/betterSessions/types.ts index b0774bed4..9026d5313 100644 --- a/src/plugins/showTimeouts/index.ts +++ b/src/plugins/betterSessions/types.ts @@ -16,20 +16,17 @@ * along with this program. If not, see . */ -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +export interface SessionInfo { + session: { + id_hash: string; + approx_last_used_time: Date; + client_info: { + os: string; + platform: string; + location: string; + }; + }, + current?: boolean; +} -export default definePlugin({ - name: "ShowTimeouts", - description: "Display member timeout icons in chat regardless of permissions.", - authors: [Devs.Dolfies], - patches: [ - { - find: "showCommunicationDisabledStyles", - replacement: { - match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/, - replace: "", - }, - }, - ], -}); +export type Session = SessionInfo["session"]; diff --git a/src/plugins/betterSessions/utils.ts b/src/plugins/betterSessions/utils.ts new file mode 100644 index 000000000..3015dc47c --- /dev/null +++ b/src/plugins/betterSessions/utils.ts @@ -0,0 +1,90 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 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 { DataStore } from "@api/index"; +import { UserStore } from "@webpack/common"; + +import { ChromeIcon, DiscordIcon, EdgeIcon, FirefoxIcon, IEIcon, MobileIcon, OperaIcon, SafariIcon, UnknownIcon } from "./components/icons"; +import { SessionInfo } from "./types"; + +const getDataKey = () => `BetterSessions_savedSessions_${UserStore.getCurrentUser().id}`; + +export const savedSessionsCache: Map = new Map(); + +export function getDefaultName(clientInfo: SessionInfo["session"]["client_info"]) { + return `${clientInfo.os} · ${clientInfo.platform}`; +} + +export function saveSessionsToDataStore() { + return DataStore.set(getDataKey(), savedSessionsCache); +} + +export async function fetchNamesFromDataStore() { + const savedSessions = await DataStore.get>(getDataKey()) || new Map(); + savedSessions.forEach((data, idHash) => { + savedSessionsCache.set(idHash, data); + }); +} + +export function GetOsColor(os: string) { + switch (os) { + case "Windows Mobile": + case "Windows": + return "#55a6ef"; // Light blue + case "Linux": + return "#cdcd31"; // Yellow + case "Android": + return "#7bc958"; // Green + case "Mac OS X": + case "iOS": + return ""; // Default to white/black (theme-dependent) + default: + return "#f3799a"; // Pink + } +} + +export function GetPlatformIcon(platform: string) { + switch (platform) { + case "Discord Android": + case "Discord iOS": + case "Discord Client": + return DiscordIcon; + case "Android Chrome": + case "Chrome iOS": + case "Chrome": + return ChromeIcon; + case "Edge": + return EdgeIcon; + case "Firefox": + return FirefoxIcon; + case "Internet Explorer": + return IEIcon; + case "Opera Mini": + case "Opera": + return OperaIcon; + case "Mobile Safari": + case "Safari": + return SafariIcon; + case "BlackBerry": + case "Facebook Mobile": + case "Android Mobile": + return MobileIcon; + default: + return UnknownIcon; + } +} diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx index e70f8c908..334372e38 100644 --- a/src/plugins/customRPC/index.tsx +++ b/src/plugins/customRPC/index.tsx @@ -22,12 +22,12 @@ import { Devs } from "@utils/constants"; import { isTruthy } from "@utils/guards"; import { useAwaiter } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; +const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color"); const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile"); const ActivityClassName = findByPropsLazy("activity", "buttonColor"); -const Colors = findByPropsLazy("profileColors"); async function getApplicationAsset(key: string): Promise { if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, ""); @@ -393,6 +393,8 @@ export default definePlugin({ settingsAboutComponent: () => { const activity = useAwaiter(createActivity); + const { profileThemeStyle } = useProfileThemeStyle({}); + return ( <> @@ -406,7 +408,7 @@ export default definePlugin({ If you want to use image link, download your image and reupload the image to Imgur and get the image link by right-clicking the image and select "Copy image address". -
+
{activity[0] && { + for (const name of ["memberSinceWrapper", "memberSinceContainer"]) { + const mod = find(filters.byProps(name), { isIndirect: true }); + if (mod) return mod[name]; + } + handleModuleNotFound("findByProps", "memberSinceWrapper/memberSinceContainer"); + return ""; +}); const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate"); const clydeMoreInfo = findByPropsLazy("clydeMoreInfo"); const locale = findByPropsLazy("getLocale"); @@ -49,7 +59,7 @@ export default definePlugin({ Friends Since -
+
{!!getCurrentChannel()?.guild_id && ( . +*/ + +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByProps, findStoreLazy } from "@webpack"; +import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common"; +import { Settings } from "Vencord"; + +const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore"); + +interface UserAffinity { + user_id: string; + affinity: number; +} + +export default definePlugin({ + name: "ImplicitRelationships", + description: "Shows your implicit relationships in the Friends tab.", + authors: [Devs.Dolfies], + patches: [ + // Counts header + { + find: ".FRIENDS_ALL_HEADER", + replacement: { + match: /toString\(\)\}\);case (\i\.\i)\.BLOCKED/, + replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED' + }, + }, + // No friends page + { + find: "FriendsEmptyState: Invalid empty state", + replacement: { + match: /case (\i\.\i)\.ONLINE:(?=return (\i)\.SECTION_ONLINE)/, + replace: "case $1.ONLINE:case $1.IMPLICIT:" + }, + }, + // Sections header + { + find: ".FRIENDS_SECTION_ONLINE", + replacement: { + match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.Messages\.BLOCKED\}\)/, + replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&" + }, + }, + // Sections content + { + find: '"FriendsStore"', + replacement: { + match: /(?<=case (\i\.\i)\.BLOCKED:return (\i)\.type===\i\.\i\.BLOCKED)/, + replace: ";case $1.IMPLICIT:return $2.type===5" + }, + }, + // Piggyback relationship fetch + { + find: ".fetchRelationships()", + replacement: { + match: /(\i\.\i)\.fetchRelationships\(\)/, + // This relationship fetch is actually completely useless, but whatevs + replace: "$1.fetchRelationships(),$self.fetchImplicitRelationships()" + }, + }, + // Modify sort -- thanks megu for the patch (from sortFriendRequests) + { + find: "getRelationshipCounts(){", + replacement: { + predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity, + match: /\.sortBy\(\i=>\i\.comparator\)/, + replace: "$&.sortBy((row) => $self.sortList(row))" + } + }, + + // Add support for the nonce parameter to Discord's shitcode + { + find: ".REQUEST_GUILD_MEMBERS", + replacement: { + match: /\.send\(8,{/, + replace: "$&nonce:arguments[1].nonce," + } + }, + { + find: "GUILD_MEMBERS_REQUEST:", + replacement: { + match: /presences:!!(\i)\.presences/, + replace: "$&,nonce:$1.nonce" + }, + }, + { + find: ".not_found", + replacement: { + match: /notFound:(\i)\.not_found/, + replace: "$&,nonce:$1.nonce" + }, + } + ], + settings: definePluginSettings( + { + sortByAffinity: { + type: OptionType.BOOLEAN, + default: true, + description: "Whether to sort implicit relationships by their affinity to you.", + restartNeeded: true + }, + } + ), + + sortList(row: any) { + return row.type === 5 + ? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0 + : row.comparator; + }, + + async fetchImplicitRelationships() { + // Implicit relationships are defined as users that you: + // 1. Have an affinity for + // 2. Do not have a relationship with // TODO: Check how this works with pending/blocked relationships + // 3. Have a mutual guild with + const userAffinities: Set = UserAffinitiesStore.getUserAffinitiesUserIds(); + const nonFriendAffinities = Array.from(userAffinities).filter( + id => !RelationshipStore.getRelationshipType(id) + ); + + // I would love to just check user cache here (falling back to the gateway of course) + // However, users in user cache may just be there because they share a DM or group DM with you + // So there's no guarantee that a user being in user cache means they have a mutual with you + // To get around this, we request users we have DMs with, and ignore them below if we don't get them back + const dmUserIds = new Set( + Object.values(ChannelStore.getSortedPrivateChannels()).flatMap(c => c.recipients) + ); + const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id) || dmUserIds.has(id)); + const allGuildIds = Object.keys(GuildStore.getGuilds()); + const sentNonce = SnowflakeUtils.fromTimestamp(Date.now()); + let count = allGuildIds.length * Math.ceil(toRequest.length / 100); + + // OP 8 Request Guild Members allows 100 user IDs at a time + const ignore = new Set(toRequest); + const relationships = RelationshipStore.getRelationships(); + const callback = ({ nonce, members }) => { + if (nonce !== sentNonce) return; + members.forEach(member => { + ignore.delete(member.user.id); + }); + + nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5); + RelationshipStore.emitChange(); + if (--count === 0) { + FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK", callback); + } + }; + + FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK", callback); + for (let i = 0; i < toRequest.length; i += 100) { + FluxDispatcher.dispatch({ + type: "GUILD_MEMBERS_REQUEST", + guildIds: allGuildIds, + userIds: toRequest.slice(i, i + 100), + nonce: sentNonce, + }); + } + }, + + start() { + const { FriendsSections } = findByProps("FriendsSections"); + FriendsSections.IMPLICIT = "IMPLICIT"; + } +}); diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 8a1e23912..c8b464391 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -337,12 +337,12 @@ export default definePlugin({ { // Attachment renderer // Module 96063 - find: ".removeAttachmentHoverButton", + find: ".removeMosaicItemHoverButton", group: true, replacement: [ { - match: /(className:\i,attachment:\i),/, - replace: "$1,attachment: {deleted}," + match: /(className:\i,item:\i),/, + replace: "$1,item: deleted," }, { match: /\[\i\.obscured\]:.+?,/, diff --git a/src/plugins/noMosaic/index.ts b/src/plugins/noMosaic/index.ts index 802f65e51..4715bde5c 100644 --- a/src/plugins/noMosaic/index.ts +++ b/src/plugins/noMosaic/index.ts @@ -5,15 +5,9 @@ */ import { definePluginSettings } from "@api/Settings"; -import { disableStyle, enableStyle } from "@api/Styles"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import style from "./styles.css?managed"; - -const MAX_WIDTH = 550; -const MAX_HEIGHT = 350; - const settings = definePluginSettings({ inlineVideo: { description: "Play videos without carousel modal", @@ -33,15 +27,11 @@ export default definePlugin({ patches: [ { - find: ".oneByTwoLayoutThreeGrid", - replacement: [{ - match: /mediaLayoutType:\i\.\i\.MOSAIC/, - replace: "mediaLayoutType:'RESPONSIVE'", - }, - { - match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/, - replace: '"INVALID"', - }] + find: "isGroupableMedia:function()", + replacement: { + match: /=>"IMAGE"===\i\|\|"VIDEO"===\i;/, + replace: "=>false;" + } }, { find: "renderAttachments(", @@ -51,52 +41,5 @@ export default definePlugin({ replace: "$&$1.content_type?.startsWith('image/')&&" } }, - { - find: "Messages.REMOVE_ATTACHMENT_TOOLTIP_TEXT", - replacement: [{ - match: /\i===\i\.\i\.MOSAIC/, - replace: "true" - }, - { - match: /\i!==\i\.\i\.MOSAIC/, - replace: "false" - }] - }, - { - find: ".messageAttachment,", - replacement: { - match: /\{width:\i,height:\i\}=(\i).*?(?=className:\i\(\)\(\i\.messageAttachment,)/, - replace: "$&style:$self.style($1)," - } - } - ], - - style({ width, height }) { - if (!width || !height) return {}; - - if (width > MAX_WIDTH || height > MAX_HEIGHT) { - if (width / height > MAX_WIDTH / MAX_HEIGHT) { - height = Math.ceil(MAX_WIDTH / (width / height)); - width = MAX_WIDTH; - } else { - width = Math.ceil(MAX_HEIGHT * (width / height)); - height = MAX_HEIGHT; - } - } - - return { - maxWidth: width, - width: "100%", - aspectRatio: `${width} / ${height}` - }; - - }, - - start() { - enableStyle(style); - }, - - stop() { - disableStyle(style); - } + ] }); diff --git a/src/plugins/noMosaic/styles.css b/src/plugins/noMosaic/styles.css deleted file mode 100644 index 3a8a8e464..000000000 --- a/src/plugins/noMosaic/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -[class^="nonMediaAttachmentsContainer_"] [class*="messageAttachment_"] { - position: relative; -} - -[class^="nonMediaAttachmentsContainer_"], -[class^="nonMediaAttachmentItem_"]:has([class^="messageAttachment_"][style^="max-width"]) { - width: 100%; -} diff --git a/src/plugins/onePingPerDM/index.ts b/src/plugins/onePingPerDM/index.ts index ae38343db..e9cde6523 100644 --- a/src/plugins/onePingPerDM/index.ts +++ b/src/plugins/onePingPerDM/index.ts @@ -49,7 +49,7 @@ export default definePlugin({ replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else " }, { - match: /sound:(\i\?\i:void 0,volume:\i,onClick)/, + match: /sound:(\i\?\i:void 0,soundpack:\i,volume:\i,onClick)/, replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1" }] }], diff --git a/src/plugins/pictureInPicture/index.tsx b/src/plugins/pictureInPicture/index.tsx index ca766affc..0a22f06db 100644 --- a/src/plugins/pictureInPicture/index.tsx +++ b/src/plugins/pictureInPicture/index.tsx @@ -10,7 +10,7 @@ import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { React, Tooltip } from "@webpack/common"; +import { Tooltip } from "@webpack/common"; const settings = definePluginSettings({ loop: { @@ -28,9 +28,9 @@ export default definePlugin({ settings, patches: [ { - find: ".nonMediaAttachment]", + find: ".nonMediaMosaicItem]", replacement: { - match: /\.nonMediaAttachment\]:!(\i).{0,10}children:\[(\S)/, + match: /\.nonMediaMosaicItem\]:!(\i).{0,10}children:\[(\S)/, replace: "$&,$1&&$2&&$self.renderPiPButton()," }, }, diff --git a/src/plugins/pronoundb/index.ts b/src/plugins/pronoundb/index.ts index b1e74158b..61edd191e 100644 --- a/src/plugins/pronoundb/index.ts +++ b/src/plugins/pronoundb/index.ts @@ -36,25 +36,24 @@ export default definePlugin({ authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven], description: "Adds pronouns to user messages using pronoundb", patches: [ - // Add next to username (compact mode) { find: "showCommunicationDisabledStyles", - replacement: { - match: /("span",{id:\i,className:\i,children:\i}\))/, - replace: "$1, $self.CompactPronounsChatComponentWrapper(arguments[0])" - } - }, - // Patch the chat timestamp element (normal mode) - { - find: "showCommunicationDisabledStyles", - replacement: { - match: /(?<=return\s*\(0,\i\.jsxs?\)\(.+!\i&&)(\(0,\i.jsxs?\)\(.+?\{.+?\}\))/, - replace: "[$1, $self.PronounsChatComponentWrapper(arguments[0])]" - } + replacement: [ + // Add next to username (compact mode) + { + match: /("span",{id:\i,className:\i,children:\i}\))/, + replace: "$1, $self.CompactPronounsChatComponentWrapper(arguments[0])" + }, + // Patch the chat timestamp element (normal mode) + { + match: /(?<=return\s*\(0,\i\.jsxs?\)\(.+!\i&&)(\(0,\i.jsxs?\)\(.+?\{.+?\}\))/, + replace: "[$1, $self.PronounsChatComponentWrapper(arguments[0])]" + } + ] }, // Patch the profile popout username header to use our pronoun hook instead of Discord's pronouns { - find: ".userTagNoNickname", + find: ".pronouns,children", replacement: [ { match: /{user:(\i),[^}]*,pronouns:(\i),[^}]*}=\i;/, diff --git a/src/plugins/resurrectHome/index.tsx b/src/plugins/resurrectHome/index.tsx index 24cdf2b02..980629126 100644 --- a/src/plugins/resurrectHome/index.tsx +++ b/src/plugins/resurrectHome/index.tsx @@ -18,9 +18,68 @@ import { findGroupChildrenByChildId } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Menu } from "@webpack/common"; +import { findByPropsLazy } from "@webpack"; +import { Button, Menu, Tooltip, useEffect, useState } from "@webpack/common"; + +const ChannelRowClasses = findByPropsLazy("modeConnected", "modeLocked", "icon"); + +let currentShouldViewServerHome = false; +const shouldViewServerHomeStates = new Set>>(); + +function ViewServerHomeButton() { + return ( + + {tooltipProps => ( + + )} + + ); +} + +function useForceServerHome() { + const { forceServerHome } = settings.use(["forceServerHome"]); + const [shouldViewServerHome, setShouldViewServerHome] = useState(currentShouldViewServerHome); + + useEffect(() => { + shouldViewServerHomeStates.add(setShouldViewServerHome); + + return () => { + shouldViewServerHomeStates.delete(setShouldViewServerHome); + }; + }, []); + + return shouldViewServerHome || forceServerHome; +} + +function useDisableViewServerHome() { + useEffect(() => () => { + currentShouldViewServerHome = false; + for (const setState of shouldViewServerHomeStates) { + setState(false); + } + }, []); +} const settings = definePluginSettings({ forceServerHome: { @@ -30,12 +89,6 @@ const settings = definePluginSettings({ } }); -function useForceServerHome() { - const { forceServerHome } = settings.use(["forceServerHome"]); - - return forceServerHome; -} - export default definePlugin({ name: "ResurrectHome", description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.", @@ -92,14 +145,37 @@ export default definePlugin({ match: /getMutableGuildChannelsForGuild\(\i\);return\(0,\i\.useStateFromStores\).+?\]\)(?=}function)/, replace: m => `${m}&&!$self.useForceServerHome()` } + }, + // Add View Server Home Button to Server Guide + { + find: "487e85_1", + replacement: { + match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/, + replace: "badge:$self.ViewServerHomeButton({serverGuide:$1})," + } + }, + // Disable view Server Home override when the Server Home is unmouted + { + find: "69386d_5", + replacement: { + match: /location:"69386d_5".+?;/, + replace: "$&$self.useDisableViewServerHome();" + } } ], + ViewServerHomeButton: ErrorBoundary.wrap(({ serverGuide }: { serverGuide?: boolean; }) => { + if (serverGuide !== true) return null; + + return ; + }), + useForceServerHome, + useDisableViewServerHome, contextMenus: { "guild-context"(children, props) { - const forceServerHome = useForceServerHome(); + const { forceServerHome } = settings.use(["forceServerHome"]); if (!props?.guild) return; diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index b421eb7fa..942697801 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -50,7 +50,7 @@ export default definePlugin({ patches: [ // Chat Mentions { - find: "CLYDE_AI_MENTION_COLOR:null,", + find: 'location:"UserMention', replacement: [ { match: /user:(\i),channel:(\i).{0,400}?"@"\.concat\(.+?\)/, @@ -94,7 +94,7 @@ export default definePlugin({ find: "renderPrioritySpeaker", replacement: [ { - match: /renderName\(\).{0,100}speaking:.{50,100}jsx.{5,10}{/, + match: /renderName\(\).{0,100}speaking:.{50,200}"div",{/, replace: "$&...$self.getVoiceProps(this.props)," } ], diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx index 83600e867..d70c09315 100644 --- a/src/plugins/showConnections/index.tsx +++ b/src/plugins/showConnections/index.tsx @@ -35,7 +35,7 @@ const Section = findComponentByCodeLazy(".lastSection", "children:"); const ThemeStore = findStoreLazy("ThemeStore"); const platformHooks: { useLegacyPlatformType(platform: string): string; } = findByPropsLazy("useLegacyPlatformType"); const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl"); -const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"'); +const getProfileThemeProps = findByCodeLazy(".getPreviewThemeColors", "primaryColor:"); const enum Spacing { COMPACT, @@ -74,8 +74,8 @@ interface ConnectionPlatform { icon: { lightSVG: string, darkSVG: string; }; } -const profilePopoutComponent = ErrorBoundary.wrap(({ user, displayProfile }: { user: User, displayProfile; }) => - +const profilePopoutComponent = ErrorBoundary.wrap((props: { user: User, displayProfile; }) => + ); const profilePanelComponent = ErrorBoundary.wrap(({ id }: { id: string; }) => diff --git a/src/plugins/showHiddenThings/README.md b/src/plugins/showHiddenThings/README.md new file mode 100644 index 000000000..b41e2d94d --- /dev/null +++ b/src/plugins/showHiddenThings/README.md @@ -0,0 +1,11 @@ +# ShowHiddenThings + +Displays various moderator-only elements regardless of permissions. + +## Features + +- Show member timeout icons in chat +![](https://github.com/Vendicated/Vencord/assets/47677887/75e1f6ba-8921-4188-9c2d-c9c3f9d07101) + +- Show the invites paused tooltip in the server list +![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b) diff --git a/src/plugins/showHiddenThings/index.ts b/src/plugins/showHiddenThings/index.ts new file mode 100644 index 000000000..e7be929bf --- /dev/null +++ b/src/plugins/showHiddenThings/index.ts @@ -0,0 +1,61 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 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 { definePluginSettings, migratePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +const settings = definePluginSettings({ + showTimeouts: { + type: OptionType.BOOLEAN, + description: "Show member timeout icons in chat.", + default: true, + }, + showInvitesPaused: { + type: OptionType.BOOLEAN, + description: "Show the invites paused tooltip in the server list.", + default: true, + }, +}); + +migratePluginSettings("ShowHiddenThings", "ShowTimeouts"); +export default definePlugin({ + name: "ShowHiddenThings", + tags: ["ShowTimeouts", "ShowInvitesPaused"], + description: "Displays various moderator-only elements regardless of permissions.", + authors: [Devs.Dolfies], + patches: [ + { + find: "showCommunicationDisabledStyles", + predicate: () => settings.store.showTimeouts, + replacement: { + match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/, + replace: "", + }, + }, + { + find: "useShouldShowInvitesDisabledNotif:", + predicate: () => settings.store.showInvitesPaused, + replacement: { + match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/, + replace: "true", + }, + } + ], + settings, +}); diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 62d0645d2..a9db1af9a 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -49,7 +49,7 @@ export default definePlugin({ { find: ".useCanSeeRemixBadge)", replacement: { - match: /(?<=onContextMenu:\i,children:).*?\}/, + match: /(?<=onContextMenu:\i,children:).*?\)}/, replace: "$self.renderUsername(arguments[0])}" } }, diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx index c40a18140..32579a803 100644 --- a/src/plugins/sortFriendRequests/index.tsx +++ b/src/plugins/sortFriendRequests/index.tsx @@ -42,7 +42,7 @@ export default definePlugin({ find: "getRelationshipCounts(){", replacement: { match: /\.sortBy\(\i=>\i\.comparator\)/, - replace: ".sortBy((row) => $self.sortList(row))" + replace: "$&.sortBy((row) => $self.sortList(row))" } }, { find: ".Messages.FRIEND_REQUEST_CANCEL", diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index ae28631c9..105b3b18a 100644 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ b/src/plugins/spotifyControls/PlayerComponent.tsx @@ -18,7 +18,6 @@ import "./spotifyStyles.css"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; import { debounce } from "@shared/debounce"; @@ -376,17 +375,10 @@ export function Player() { } as React.CSSProperties; return ( - ( -
-

Failed to render Spotify Modal :(

-

Check the console for errors

-
- )}> -
- - - -
-
+
+ + + +
); } diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx index d7e4f6454..06595892f 100644 --- a/src/plugins/spotifyControls/index.tsx +++ b/src/plugins/spotifyControls/index.tsx @@ -18,6 +18,7 @@ import { Settings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; +import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -49,10 +50,10 @@ export default definePlugin({ { find: "showTaglessAccountPanel:", replacement: { - // return React.createElement(AccountPanel, { ..., showTaglessAccountPanel: blah }) - match: /return ?(.{0,30}\(.{1,3},\{[^}]+?,showTaglessAccountPanel:.+?\}\))/, - // return [Player, Panel] - replace: "return [$self.renderPlayer(),$1]" + // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah }) + match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?showTaglessAccountPanel:)/, + // react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ... + replace: "$self.PanelWrapper,{VencordOriginal:$1," } }, { @@ -78,6 +79,25 @@ export default definePlugin({ } } ], + start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls), - renderPlayer: () => + + PanelWrapper({ VencordOriginal, ...props }) { + return ( + <> + ( +
+

Failed to render Spotify Modal :(

+

Check the console for errors

+
+ )} + > + +
+ + + + ); + } }); diff --git a/src/plugins/streamerModeOnStream/index.ts b/src/plugins/streamerModeOnStream/index.ts new file mode 100644 index 000000000..03b83f577 --- /dev/null +++ b/src/plugins/streamerModeOnStream/index.ts @@ -0,0 +1,45 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2024 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"; +import { FluxDispatcher, UserStore } from "@webpack/common"; + +interface StreamEvent { + streamKey: string; +} + +function toggleStreamerMode({ streamKey }: StreamEvent, value: boolean) { + if (!streamKey.endsWith(UserStore.getCurrentUser().id)) return; + + FluxDispatcher.dispatch({ + type: "STREAMER_MODE_UPDATE", + key: "enabled", + value + }); +} + +export default definePlugin({ + name: "StreamerModeOnStream", + description: "Automatically enables streamer mode when you start streaming in Discord", + authors: [Devs.Kodarru], + flux: { + STREAM_CREATE: d => toggleStreamerMode(d, true), + STREAM_DELETE: d => toggleStreamerMode(d, false) + } +}); diff --git a/src/plugins/validUser/index.tsx b/src/plugins/validUser/index.tsx index b0c77cb46..2fce693e8 100644 --- a/src/plugins/validUser/index.tsx +++ b/src/plugins/validUser/index.tsx @@ -111,19 +111,28 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio export default definePlugin({ name: "ValidUser", - description: "Fix mentions for unknown users showing up as '<@343383572805058560>' (hover over a mention to fix it)", + description: "Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)", authors: [Devs.Ven], tags: ["MentionCacheFix"], - patches: [{ - find: 'className:"mention"', - replacement: { - // mention = { react: function (data, parse, props) { if (data.userId == null) return RoleMention() else return UserMention() - match: /react(?=\(\i,\i,\i\).{0,50}return null==\i\?\(0,\i\.jsx\)\((\i\.\i),.+?jsx\)\((\i\.\i),\{className:"mention")/, - // react: (...args) => OurWrapper(RoleMention, UserMention, ...args), originalReact: theirFunc - replace: "react:(...args)=>$self.renderMention($1,$2,...args),originalReact" + patches: [ + { + find: 'className:"mention"', + replacement: { + // mention = { react: function (data, parse, props) { if (data.userId == null) return RoleMention() else return UserMention() + match: /react(?=\(\i,\i,\i\).{0,100}return null==.{0,70}\?\(0,\i\.jsx\)\((\i\.\i),.+?jsx\)\((\i\.\i),\{className:"mention")/, + // react: (...args) => OurWrapper(RoleMention, UserMention, ...args), originalReact: theirFunc + replace: "react:(...args)=>$self.renderMention($1,$2,...args),originalReact" + } + }, + { + find: "unknownUserMentionPlaceholder:", + replacement: { + match: /unknownUserMentionPlaceholder:/, + replace: "$&false&&" + } } - }], + ], renderMention(RoleMention, UserMention, data, parse, props) { return ( diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx index 6eb773c28..f71777ad7 100644 --- a/src/plugins/viewIcons/index.tsx +++ b/src/plugins/viewIcons/index.tsx @@ -174,7 +174,7 @@ export default definePlugin({ find: ".NITRO_BANNER,", replacement: { // style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl, - match: /style:\{(?=backgroundImage:(\i)\?"url\("\.concat\((\i),)/, + match: /style:\{(?=backgroundImage:(null!=\i)\?"url\("\.concat\((\i),)/, replace: // onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0, 'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,' diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 968f29acb..d34b61bb0 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -431,6 +431,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "newwares", id: 421405303951851520n }, + Kodarru: { + name: "Kodarru", + id: 785227396218748949n + }, nakoyasha: { name: "nakoyasha", id: 222069018507345921n @@ -442,6 +446,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({ Byron: { name: "byeoon", id: 1167275288036655133n + }, + Kaitlyn: { + name: "kaitlyn", + id: 306158896630988801n + }, + PolisanTheEasyNick: { + name: "Oleh Polisan", + id: 242305263313485825n } } satisfies Record); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 0790e8bf1..564da4813 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -92,7 +92,7 @@ if (IS_DEV && IS_DISCORD_DESKTOP) { }, 0); } -function handleModuleNotFound(method: string, ...filter: unknown[]) { +export function handleModuleNotFound(method: string, ...filter: unknown[]) { const err = new Error(`webpack.${method} found no module`); logger.error(err, "Filter:", filter); @@ -406,13 +406,15 @@ export function findExportedComponentLazy(...props: stri }); } +const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\((\[\i\.\i\(".+?"\).+?\])\)|Promise\.resolve\(\)).then\(\i\.bind\(\i,"(.+?)"\)\)/; + /** * Extract and load chunks using their entry point * @param code An array of all the code the module factory containing the lazy chunk loading must include * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory * @returns A promise that resolves when the chunks were loaded */ -export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/) { +export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) { const module = findModuleFactory(...code); if (!module) { const err = new Error("extractAndLoadChunks: Couldn't find module factory"); @@ -434,7 +436,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Pr } const [, rawChunkIds, entryPointId] = match; - if (!rawChunkIds || Number.isNaN(entryPointId)) { + if (Number.isNaN(entryPointId)) { const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number"); logger.warn(err, "Code:", code, "Matcher:", matcher); @@ -445,9 +447,11 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Pr return; } - const chunkIds = Array.from(rawChunkIds.matchAll(/\("(.+?)"\)/g)).map((m: any) => m[1]); + if (rawChunkIds) { + const chunkIds = Array.from(rawChunkIds.matchAll(/\("(.+?)"\)/g)).map((m: any) => m[1]); + await Promise.all(chunkIds.map(id => wreq.e(id))); + } - await Promise.all(chunkIds.map(id => wreq.e(id))); wreq(entryPointId); } @@ -459,7 +463,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Pr * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory * @returns A function that returns a promise that resolves when the chunks were loaded, on first call */ -export function extractAndLoadChunksLazy(code: string[], matcher: RegExp = /Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/) { +export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) { if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); return () => extractAndLoadChunks(code, matcher);