diff --git a/browser/GMPolyfill.js b/browser/GMPolyfill.js index f8801551..387389ce 100644 --- a/browser/GMPolyfill.js +++ b/browser/GMPolyfill.js @@ -62,7 +62,7 @@ function GM_fetch(url, opt) { resp.arrayBuffer = () => blobTo("arrayBuffer", blob); resp.text = () => blobTo("text", blob); resp.json = async () => JSON.parse(await blobTo("text", blob)); - resp.headers = new Headers(parseHeaders(resp.responseHeaders)); + resp.headers = parseHeaders(resp.responseHeaders); resp.ok = resp.status >= 200 && resp.status < 300; resolve(resp); }; diff --git a/package.json b/package.json index dde55d31..9d7b6347 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.7.0", + "version": "1.7.2", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 0a17e8d7..33b099ef 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -428,10 +428,11 @@ function runTime(token: string) { if (searchType === "findComponent") method = "find"; if (searchType === "findExportedComponent") method = "findByProps"; - if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") { + if (searchType === "waitFor" || searchType === "waitForComponent") { if (typeof args[0] === "string") method = "findByProps"; else method = "find"; } + if (searchType === "waitForStore") method = "findStore"; try { let result: any; diff --git a/src/api/ContextMenu.ts b/src/api/ContextMenu.ts index d66d98c4..fdd4facf 100644 --- a/src/api/ContextMenu.ts +++ b/src/api/ContextMenu.ts @@ -17,22 +17,20 @@ */ import { Logger } from "@utils/Logger"; +import { Menu, React } from "@webpack/common"; import type { ReactElement } from "react"; -type ContextMenuPatchCallbackReturn = (() => void) | void; /** * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example - * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; +export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => void; /** * @param navId The navId of the context menu being patched * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example - * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; +export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => void; const ContextMenuLogger = new Logger("ContextMenu"); @@ -93,14 +91,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba * @param id The id of the child. If an array is specified, all ids will be tried * @param children The context menu children */ -export function findGroupChildrenByChildId(id: string | string[], children: Array, _itemsArray?: Array): Array | null { +export function findGroupChildrenByChildId(id: string | string[], children: Array): Array | null { for (const child of children) { if (child == null) continue; + if (Array.isArray(child)) { + const found = findGroupChildrenByChildId(id, child); + if (found !== null) return found; + } + if ( (Array.isArray(id) && id.some(id => child.props?.id === id)) || child.props?.id === id - ) return _itemsArray ?? null; + ) return children; let nextChildren = child.props?.children; if (nextChildren) { @@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra child.props.children = nextChildren; } - const found = findGroupChildrenByChildId(id, nextChildren, nextChildren); + const found = findGroupChildrenByChildId(id, nextChildren); if (found !== null) return found; } } @@ -126,9 +129,12 @@ interface ContextMenuProps { onClose: (callback: (...args: Array) => any) => void; } -const patchedMenus = new WeakSet(); +export function _usePatchContextMenu(props: ContextMenuProps) { + props = { + ...props, + children: cloneMenuChildren(props.children), + }; -export function _patchContextMenu(props: ContextMenuProps) { props.contextMenuApiArguments ??= []; const contextMenuPatches = navPatches.get(props.navId); @@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) { if (contextMenuPatches) { for (const patch of contextMenuPatches) { try { - const callback = patch(props.children, ...props.contextMenuApiArguments); - if (!patchedMenus.has(props)) callback?.(); + patch(props.children, ...props.contextMenuApiArguments); } catch (err) { ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); } @@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) { for (const patch of globalPatches) { try { - const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments); - if (!patchedMenus.has(props)) callback?.(); + patch(props.navId, props.children, ...props.contextMenuApiArguments); } catch (err) { ContextMenuLogger.error("Global patch errored,", err); } } - patchedMenus.add(props); + return props; +} + +function cloneMenuChildren(obj: ReactElement | Array | null) { + if (Array.isArray(obj)) { + return obj.map(cloneMenuChildren); + } + + if (React.isValidElement(obj)) { + obj = React.cloneElement(obj); + + if ( + obj?.props?.children && + (obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null) + ) { + obj.props.children = cloneMenuChildren(obj.props.children); + } + } + + return obj; } diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 004a8988..c1ff6915 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -223,13 +223,13 @@ export const Settings = makeProxy(settings); export function useSettings(paths?: UseSettings[]) { const [, forceUpdate] = React.useReducer(() => ({}), {}); - const onUpdate: SubscriptionCallback = paths - ? (value, path) => paths.includes(path as UseSettings) && forceUpdate() - : forceUpdate; + if (paths) { + (forceUpdate as SubscriptionCallback)._paths = paths; + } React.useEffect(() => { - subscriptions.add(onUpdate); - return () => void subscriptions.delete(onUpdate); + subscriptions.add(forceUpdate); + return () => void subscriptions.delete(forceUpdate); }, []); return Settings; @@ -253,8 +253,10 @@ type ResolvePropDeep = P extends "" ? T : export function addSettingsListener(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void; export function addSettingsListener(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep, path: Path extends "" ? string : Path) => void): void; export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) { - if (path) + if (path) { ((onUpdate as SubscriptionCallback)._paths ??= []).push(path); + } + subscriptions.add(onUpdate); } diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx index 0392a451..080dd8dd 100644 --- a/src/components/VencordSettings/CloudTab.tsx +++ b/src/components/VencordSettings/CloudTab.tsx @@ -39,9 +39,7 @@ function validateUrl(url: string) { async function eraseAllData() { const res = await fetch(new URL("/v1/", getCloudUrl()), { method: "DELETE", - headers: new Headers({ - Authorization: await getCloudAuth() - }) + headers: { Authorization: await getCloudAuth() } }); if (!res.ok) { diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 47d400eb..3ac8a14c 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -23,7 +23,7 @@ import { debounce } from "@utils/debounce"; import { IpcEvents } from "@utils/IpcEvents"; import { Queue } from "@utils/Queue"; import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; -import { mkdirSync, readFileSync, watch } from "fs"; +import { FSWatcher, mkdirSync, readFileSync, watch } from "fs"; import { open, readdir, readFile, writeFile } from "fs/promises"; import { join, normalize } from "path"; @@ -126,16 +126,23 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => { export function initIpc(mainWindow: BrowserWindow) { + let quickCssWatcher: FSWatcher | undefined; + open(QUICKCSS_PATH, "a+").then(fd => { fd.close(); - watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { + quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); - }); + }).catch(() => { }); - watch(THEMES_DIR, { persistent: false }, debounce(() => { + const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => { mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0); })); + + mainWindow.once("closed", () => { + quickCssWatcher?.close(); + themesWatcher.close(); + }); } ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts index 2ff3ba51..20a5d700 100644 --- a/src/main/updater/git.ts +++ b/src/main/updater/git.ts @@ -49,9 +49,12 @@ async function getRepo() { async function calculateGitChanges() { await git("fetch"); - const branch = await git("branch", "--show-current"); + const branch = (await git("branch", "--show-current")).stdout.trim(); - const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s"); + const existsOnOrigin = (await git("ls-remote", "origin", branch)).stdout.length > 0; + if (!existsOnOrigin) return []; + + const res = await git("log", `HEAD...origin/${branch}`, "--pretty=format:%an/%h/%s"); const commits = res.stdout.trim(); return commits ? commits.split("\n").map(line => { diff --git a/src/plugins/_api/contextMenu.ts b/src/plugins/_api/contextMenu.ts index 55fdf3ea..01619546 100644 --- a/src/plugins/_api/contextMenu.ts +++ b/src/plugins/_api/contextMenu.ts @@ -22,15 +22,15 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "ContextMenuAPI", description: "API for adding/removing items to/from context menus.", - authors: [Devs.Nuckyz, Devs.Ven], + authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi], required: true, patches: [ { find: "♫ (つ。◕‿‿◕。)つ ♪", replacement: { - match: /let{navId:/, - replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&" + match: /(?=let{navId:)(?<=function \i\((\i)\).+?)/, + replace: "$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);" } }, { diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 6f43b76a..01220eb4 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -30,21 +30,21 @@ export default definePlugin({ authors: [Devs.Ven, Devs.Megu], required: true, - start() { + contextMenus: { // The settings shortcuts in the user settings cog context menu // read the elements from a hardcoded map which for obvious reason // doesn't contain our sections. This patches the actions of our // sections to manually use SettingsRouter (which only works on desktop // but the context menu is usually not available on mobile anyway) - addContextMenuPatch("user-settings-cog", children => () => { - const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any; + "user-settings-cog"(children) { + const section = findGroupChildrenByChildId("VencordSettings", children); section?.forEach(c => { const id = c?.props?.id; if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) { - c.props.action = () => SettingsRouter.open(id); + c!.props.action = () => SettingsRouter.open(id); } }); - }); + } }, patches: [{ diff --git a/src/plugins/betterRoleContext/README.md b/src/plugins/betterRoleContext/README.md new file mode 100644 index 00000000..3f3086bd --- /dev/null +++ b/src/plugins/betterRoleContext/README.md @@ -0,0 +1,6 @@ +# BetterRoleContext + +Adds options to copy role color and edit role when right clicking roles in the user profile + +![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326) + diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx new file mode 100644 index 00000000..e73779ad --- /dev/null +++ b/src/plugins/betterRoleContext/index.tsx @@ -0,0 +1,81 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import { getCurrentGuild, getGuildRoles } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; + +const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild"); + +function PencilIcon() { + return ( + + + + ); +} + +function AppearanceIcon() { + return ( + + + + ); +} + +export default definePlugin({ + name: "BetterRoleContext", + description: "Adds options to copy role color / edit role when right clicking roles in the user profile", + authors: [Devs.Ven], + + start() { + // DeveloperMode needs to be enabled for the context menu to be shown + TextAndImagesSettingsStores.DeveloperMode.updateSetting(true); + }, + + contextMenus: { + "dev-context"(children, { id }: { id: string; }) { + const guild = getCurrentGuild(); + if (!guild) return; + + const role = getGuildRoles(guild.id)[id]; + if (!role) return; + + if (role.colorString) { + children.push( + Clipboard.copy(role.colorString!)} + icon={AppearanceIcon} + /> + ); + } + + if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) { + children.push( + { + await GuildSettingsActions.open(guild.id, "ROLES"); + GuildSettingsActions.selectRole(id); + }} + icon={PencilIcon} + /> + ); + } + } + } +}); diff --git a/src/plugins/biggerStreamPreview/index.tsx b/src/plugins/biggerStreamPreview/index.tsx index 40bbe30a..8cca912b 100644 --- a/src/plugins/biggerStreamPreview/index.tsx +++ b/src/plugins/biggerStreamPreview/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { ScreenshareIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { openImageModal } from "@utils/discord"; @@ -60,7 +60,7 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica openImageModal(previewUrl); }; -export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => () => { +export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => { const stream = ApplicationStreamingStore.getAnyStreamForUser(userId); if (!stream) return; @@ -89,12 +89,8 @@ export default definePlugin({ name: "BiggerStreamPreview", description: "This plugin allows you to enlarge stream previews", authors: [Devs.phil], - start: () => { - addContextMenuPatch("user-context", userContextPatch); - addContextMenuPatch("stream-context", streamContextPatch); - }, - stop: () => { - removeContextMenuPatch("user-context", userContextPatch); - removeContextMenuPatch("stream-context", streamContextPatch); + contextMenus: { + "user-context": userContextPatch, + "stream-context": streamContextPatch } }); diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index 5d8cd4dc..4e07daf4 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -12,7 +12,7 @@ import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import definePlugin, { OptionType, StartAt } from "@utils/types"; import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common"; +import { Button, Forms, useStateFromStores } from "@webpack/common"; const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); @@ -200,8 +200,8 @@ function captureOne(str, regex) { return (result === null) ? null : result[1]; } -function mapReject(arr, mapFunc, rejectFunc = _.isNull) { - return _.reject(arr.map(mapFunc), rejectFunc); +function mapReject(arr, mapFunc) { + return arr.map(mapFunc).filter(Boolean); } function updateColorVars(color: string) { diff --git a/src/plugins/copyUserURLs/index.tsx b/src/plugins/copyUserURLs/index.tsx index 9f69674c..7af8502d 100644 --- a/src/plugins/copyUserURLs/index.tsx +++ b/src/plugins/copyUserURLs/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { LinkIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -29,7 +29,7 @@ interface UserContextProps { user: User; } -const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => { +const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => { if (!user) return; children.push( @@ -46,12 +46,7 @@ export default definePlugin({ name: "CopyUserURLs", authors: [Devs.castdrian], description: "Adds a 'Copy User URL' option to the user context menu.", - - start() { - addContextMenuPatch("user-context", UserContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("user-context", UserContextMenuPatch); - }, + contextMenus: { + "user-context": UserContextMenuPatch + } }); diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx index 3653a077..e70f8c90 100644 --- a/src/plugins/customRPC/index.tsx +++ b/src/plugins/customRPC/index.tsx @@ -175,7 +175,7 @@ const settings = definePluginSettings({ }, startTime: { type: OptionType.NUMBER, - description: "Start timestamp (only for custom timestamp mode)", + description: "Start timestamp in milisecond (only for custom timestamp mode)", onChange: onChange, disabled: isTimestampDisabled, isValid: (value: number) => { @@ -185,7 +185,7 @@ const settings = definePluginSettings({ }, endTime: { type: OptionType.NUMBER, - description: "End timestamp (only for custom timestamp mode)", + description: "End timestamp in milisecond (only for custom timestamp mode)", onChange: onChange, disabled: isTimestampDisabled, isValid: (value: number) => { @@ -313,12 +313,12 @@ async function createActivity(): Promise { switch (settings.store.timestampMode) { case TimestampMode.NOW: activity.timestamps = { - start: Math.floor(Date.now() / 1000) + start: Date.now() }; break; case TimestampMode.TIME: activity.timestamps = { - start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds() + start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000 }; break; case TimestampMode.CUSTOM: diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 8cfd8c03..713b17d7 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -131,9 +131,10 @@ export default definePlugin({ getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) { // Only Decor avatar decorations have this SKU ID if (avatarDecoration?.skuId === SKU_ID) { - const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`); - url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString()); - return url.toString(); + const parts = avatarDecoration.asset.split("_"); + // Remove a_ prefix if it's animated and animation is disabled + if (isAnimatedAvatarDecoration(avatarDecoration.asset) && !canAnimate) parts.shift(); + return `${CDN_URL}/${parts.join("_")}.png`; } else if (avatarDecoration?.skuId === RAW_SKU_ID) { return avatarDecoration.asset; } diff --git a/src/plugins/emoteCloner/index.tsx b/src/plugins/emoteCloner/index.tsx index 219ce435..b25e1be2 100644 --- a/src/plugins/emoteCloner/index.tsx +++ b/src/plugins/emoteCloner/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; @@ -312,7 +312,7 @@ function isGifUrl(url: string) { return new URL(url).pathname.endsWith(".gif"); } -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {}; if (!favoriteableId) return; @@ -341,7 +341,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) = findGroupChildrenByChildId("copy-link", children)?.push(menuItem); }; -const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => { +const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => { const { id, name, type } = props?.target?.dataset ?? {}; if (!id) return; @@ -363,14 +363,8 @@ export default definePlugin({ description: "Allows you to clone Emotes & Stickers to your own server (right click them)", tags: ["StickerCloner"], authors: [Devs.Ven, Devs.Nuckyz], - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - addContextMenuPatch("expression-picker", expressionPickerPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - removeContextMenuPatch("expression-picker", expressionPickerPatch); + contextMenus: { + "message": messageContextMenuPatch, + "expression-picker": expressionPickerPatch } }); diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 20125c7d..68560817 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -162,7 +162,7 @@ const settings = definePluginSettings({ default: true }, hyperLinkText: { - description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji name.", + description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.", type: OptionType.STRING, default: "{{NAME}}" } @@ -369,8 +369,8 @@ export default definePlugin({ predicate: () => settings.store.transformEmojis, replacement: { // Add the fake nitro emoji notice - match: /(?<=isDiscoverable:\i,emojiComesFromCurrentGuild:\i,.+?}=(\i).+?;)(.*?return )(.{0,1000}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(?=},)/, - replace: (_, props, rest, reactNode) => `let{fakeNitroNode}=${props};${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!fakeNitroNode?.fake)` + match: /(?<=emojiDescription:)(\i)(?<=\1=\i\((\i)\).+?)/, + replace: (_, reactNode, props) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!${props}?.fakeNitroNode?.fake)` } }, // Allow using custom app icons @@ -474,7 +474,7 @@ export default definePlugin({ if (typeof firstContent === "string") { content[0] = firstContent.trimStart(); content[0] || content.shift(); - } else if (firstContent?.type === "span") { + } else if (typeof firstContent?.props?.children === "string") { firstContent.props.children = firstContent.props.children.trimStart(); firstContent.props.children || content.shift(); } @@ -484,7 +484,7 @@ export default definePlugin({ if (typeof lastContent === "string") { content[lastIndex] = lastContent.trimEnd(); content[lastIndex] || content.pop(); - } else if (lastContent?.type === "span") { + } else if (typeof lastContent?.props?.children === "string") { lastContent.props.children = lastContent.props.children.trimEnd(); lastContent.props.children || content.pop(); } @@ -585,13 +585,15 @@ export default definePlugin({ for (const [index, child] of children.entries()) children[index] = modifyChild(child); children = this.clearEmptyArrayItems(children); - this.trimContent(children); return children; }; try { - return modifyChildren(lodash.cloneDeep(content)); + const newContent = modifyChildren(lodash.cloneDeep(content)); + this.trimContent(newContent); + + return newContent; } catch (err) { new Logger("FakeNitro").error(err); return content; @@ -791,8 +793,8 @@ export default definePlugin({ title: "Hold on!", body:
- You are trying to send/edit a message that contains a FakeNitro emoji or sticker - , however you do not have permissions to embed links in the current channel. + You are trying to send/edit a message that contains a FakeNitro emoji or sticker, + however you do not have permissions to embed links in the current channel. Are you sure you want to send this message? Your FakeNitro items will appear as a link only. @@ -864,7 +866,9 @@ export default definePlugin({ const url = new URL(link); url.searchParams.set("name", sticker.name); - messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${sticker.name}](${url})` : url}`; + const linkText = s.hyperLinkText.replaceAll("{{NAME}}", sticker.name); + + messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`; extra.stickers!.length = 0; } } diff --git a/src/plugins/friendsSince/README.md b/src/plugins/friendsSince/README.md new file mode 100644 index 00000000..ccbb09b6 --- /dev/null +++ b/src/plugins/friendsSince/README.md @@ -0,0 +1,5 @@ +# FriendsSince + +Shows when you became friends with someone in the user popout + +![](https://github.com/Vendicated/Vencord/assets/45497981/bb258188-ab48-4c4d-9858-1e90ba41e926) diff --git a/src/plugins/friendsSince/index.tsx b/src/plugins/friendsSince/index.tsx new file mode 100644 index 00000000..ab3320df --- /dev/null +++ b/src/plugins/friendsSince/index.tsx @@ -0,0 +1,60 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { React, RelationshipStore } from "@webpack/common"; + +const { Heading, Text } = findByPropsLazy("Heading", "Text"); +const container = findByPropsLazy("memberSinceContainer"); +const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate"); +const clydeMoreInfo = findByPropsLazy("clydeMoreInfo"); +const locale = findByPropsLazy("getLocale"); +const lastSection = findByPropsLazy("lastSection"); + +export default definePlugin({ + name: "FriendsSince", + description: "Shows when you became friends with someone in the user popout", + authors: [Devs.Elvyra], + patches: [ + { + find: ".AnalyticsSections.USER_PROFILE}", + replacement: { + match: /\i.default,\{userId:(\i.id).{0,30}}\)/, + replace: "$&,$self.friendsSince({ userId: $1 })" + } + }, + { + find: ".UserPopoutUpsellSource.PROFILE_PANEL,", + replacement: { + match: /\i.default,\{userId:(\i)}\)/, + replace: "$&,$self.friendsSince({ userId: $1 })" + } + } + ], + + friendsSince: ErrorBoundary.wrap(({ userId }: { userId: string; }) => { + const friendsSince = RelationshipStore.getSince(userId); + if (!friendsSince) return null; + + return ( +
+ + Friends Since + + +
+ + {getCreatedAtDate(friendsSince, locale.getLocale())} + +
+
+ ); + }, { noop: true }) +}); + diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 4e747f36..c04ce1c5 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -5,12 +5,14 @@ */ import * as DataStore from "@api/DataStore"; -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; +import { Flex } from "@components/Flex"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import { Margins } from "@utils/margins"; +import definePlugin, { OptionType } from "@utils/types"; import { findStoreLazy } from "@webpack"; -import { StatusSettingsStores, Tooltip } from "webpack/common"; +import { Button, Forms, showToast, StatusSettingsStores, TextInput, Toasts, Tooltip, useEffect, useState } from "webpack/common"; const enum ActivitiesTypes { Game, @@ -69,7 +71,113 @@ function handleActivityToggle(e: React.MouseEvent StatusSettingsStores.ShowCurrentGame.updateSetting(old => old); } -const settings = definePluginSettings({}).withPrivateSettings<{ +function ImportCustomRPCComponent() { + return ( + + Import the application id of the CustomRPC plugin to the allowed list +
+ +
+
+ ); +} + +let allowedIdsPushID: ((id: string) => boolean) | null = null; + +function AllowedIdsComponent(props: { setValue: (value: string) => void; }) { + const [allowedIds, setAllowedIds] = useState(settings.store.allowedIds ?? ""); + + allowedIdsPushID = (id: string) => { + const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean)); + + const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false); + + const ids = Array.from(currentIds).join(", "); + setAllowedIds(ids); + props.setValue(ids); + + return isAlreadyAdded; + }; + + useEffect(() => () => { + allowedIdsPushID = null; + }, []); + + function handleChange(newValue: string) { + setAllowedIds(newValue); + props.setValue(newValue); + } + + return ( + + Allowed List + Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC) + + + ); +} + +const settings = definePluginSettings({ + importCustomRPC: { + type: OptionType.COMPONENT, + description: "", + component: () => + }, + allowedIds: { + type: OptionType.COMPONENT, + description: "", + default: "", + onChange(newValue: string) { + const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean)); + settings.store.allowedIds = Array.from(ids).join(", "); + }, + component: props => + }, + ignorePlaying: { + type: OptionType.BOOLEAN, + description: "Ignore all playing activities (These are usually game and RPC activities)", + default: false + }, + ignoreStreaming: { + type: OptionType.BOOLEAN, + description: "Ignore all streaming activities", + default: false + }, + ignoreListening: { + type: OptionType.BOOLEAN, + description: "Ignore all listening activities (These are usually spotify activities)", + default: false + }, + ignoreWatching: { + type: OptionType.BOOLEAN, + description: "Ignore all watching activities", + default: false + }, + ignoreCompeting: { + type: OptionType.BOOLEAN, + description: "Ignore all competing activities (These are normally special game activities)", + default: false + } +}).withPrivateSettings<{ ignoredActivities: IgnoredActivity[]; }>(); @@ -77,10 +185,26 @@ function getIgnoredActivities() { return settings.store.ignoredActivities ??= []; } +function isActivityTypeIgnored(type: number, id?: string) { + if (id && settings.store.allowedIds.includes(id)) { + return false; + } + + switch (type) { + case 0: return settings.store.ignorePlaying; + case 1: return settings.store.ignoreStreaming; + case 2: return settings.store.ignoreListening; + case 3: return settings.store.ignoreWatching; + case 5: return settings.store.ignoreCompeting; + } + + return false; +} + export default definePlugin({ name: "IgnoreActivities", authors: [Devs.Nuckyz], - description: "Ignore activities from showing up on your status ONLY. You can configure which ones are ignored from the Registered Games and Activities tabs.", + description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.", settings, @@ -141,13 +265,17 @@ export default definePlugin({ }, isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) { - if (props.type === 0 || props.type === 3) { - if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id); - else { - const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; - if (exePath) return !getIgnoredActivities().some(activity => activity.id === exePath); + if (isActivityTypeIgnored(props.type, props.application_id)) return false; + + if (props.application_id != null) { + return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id); + } else { + const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; + if (exePath) { + return !getIgnoredActivities().some(activity => activity.id === exePath); } } + return true; }, diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 8d8b6726..048c0ed5 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -16,14 +16,14 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; import { debounce } from "@utils/debounce"; import definePlugin, { OptionType } from "@utils/types"; -import { ContextMenuApi, Menu, React, ReactDOM } from "@webpack/common"; +import { Menu, React, ReactDOM } from "@webpack/common"; import type { Root } from "react-dom/client"; import { Magnifier, MagnifierProps } from "./components/Magnifier"; @@ -80,25 +80,25 @@ export const settings = definePluginSettings({ }); -const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => { +const imageContextMenuPatch: NavContextMenuPatchCallback = children => { + const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]); + children.push( { - settings.store.square = !settings.store.square; - ContextMenuApi.closeContextMenu(); + settings.store.square = !square; }} /> { - settings.store.nearestNeighbour = !settings.store.nearestNeighbour; - ContextMenuApi.closeContextMenu(); + settings.store.nearestNeighbour = !nearestNeighbour; }} /> | null, @@ -245,7 +248,6 @@ export default definePlugin({ start() { enableStyle(styles); - addContextMenuPatch("image-context", imageContextMenuPatch); this.element = document.createElement("div"); this.element.classList.add("MagnifierContainer"); document.body.appendChild(this.element); @@ -256,6 +258,5 @@ export default definePlugin({ // so componenetWillUnMount gets called if Magnifier component is still alive this.root && this.root.unmount(); this.element?.remove(); - removeContextMenuPatch("image-context", imageContextMenuPatch); } }); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 23483860..7092001e 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -17,6 +17,7 @@ */ import { registerCommand, unregisterCommand } from "@api/Commands"; +import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; import { Patch, Plugin, StartAt } from "@utils/types"; @@ -119,7 +120,7 @@ export function startDependenciesRecursive(p: Plugin) { } export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { - const { name, commands, flux } = p; + const { name, commands, flux, contextMenus } = p; if (p.start) { logger.info("Starting plugin", name); @@ -154,11 +155,17 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } } + if (contextMenus) { + for (const navId in contextMenus) { + addContextMenuPatch(navId, contextMenus[navId]); + } + } + return true; }, p => `startPlugin ${p.name}`); export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) { - const { name, commands, flux } = p; + const { name, commands, flux, contextMenus } = p; if (p.stop) { logger.info("Stopping plugin", name); if (!p.started) { @@ -192,5 +199,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } } + if (contextMenus) { + for (const navId in contextMenus) { + removeContextMenuPatch(navId, contextMenus[navId]); + } + } + return true; }, p => `stopPlugin ${p.name}`); diff --git a/src/plugins/memberCount/MemberCount.tsx b/src/plugins/memberCount/MemberCount.tsx new file mode 100644 index 00000000..50665353 --- /dev/null +++ b/src/plugins/memberCount/MemberCount.tsx @@ -0,0 +1,66 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { getCurrentChannel } from "@utils/discord"; +import { SelectedChannelStore, Tooltip, useEffect, useStateFromStores } from "@webpack/common"; + +import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat } from "."; +import { OnlineMemberCountStore } from "./OnlineMemberCountStore"; + +export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) { + const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); + + const guildId = isTooltip ? tooltipGuildId! : currentChannel.guild_id; + + const totalCount = useStateFromStores( + [GuildMemberCountStore], + () => GuildMemberCountStore.getMemberCount(guildId) + ); + + let onlineCount = useStateFromStores( + [OnlineMemberCountStore], + () => OnlineMemberCountStore.getCount(guildId) + ); + + const { groups } = useStateFromStores( + [ChannelMemberStore], + () => ChannelMemberStore.getProps(guildId, currentChannel.id) + ); + + if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) { + onlineCount = groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0); + } + + useEffect(() => { + OnlineMemberCountStore.ensureCount(guildId); + }, [guildId]); + + if (totalCount == null) + return null; + + const formattedOnlineCount = onlineCount != null ? numberFormat(onlineCount) : "?"; + + return ( +
+ + {props => ( +
+ + {formattedOnlineCount} +
+ )} +
+ + {props => ( +
+ + {numberFormat(totalCount)} +
+ )} +
+
+ ); +} diff --git a/src/plugins/memberCount/OnlineMemberCountStore.ts b/src/plugins/memberCount/OnlineMemberCountStore.ts new file mode 100644 index 00000000..8790f5e2 --- /dev/null +++ b/src/plugins/memberCount/OnlineMemberCountStore.ts @@ -0,0 +1,52 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { proxyLazy } from "@utils/lazy"; +import { sleep } from "@utils/misc"; +import { Queue } from "@utils/Queue"; +import { Flux, FluxDispatcher, GuildChannelStore, PrivateChannelsStore } from "@webpack/common"; + +export const OnlineMemberCountStore = proxyLazy(() => { + const preloadQueue = new Queue(); + + const onlineMemberMap = new Map(); + + class OnlineMemberCountStore extends Flux.Store { + getCount(guildId: string) { + return onlineMemberMap.get(guildId); + } + + async _ensureCount(guildId: string) { + if (onlineMemberMap.has(guildId)) return; + + await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id); + } + + ensureCount(guildId: string) { + if (onlineMemberMap.has(guildId)) return; + + preloadQueue.push(() => + this._ensureCount(guildId) + .then( + () => sleep(200), + () => sleep(200) + ) + ); + } + } + + return new OnlineMemberCountStore(FluxDispatcher, { + GUILD_MEMBER_LIST_UPDATE({ guildId, groups }: { guildId: string, groups: { count: number; id: string; }[]; }) { + onlineMemberMap.set( + guildId, + groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0) + ); + }, + ONLINE_GUILD_MEMBER_COUNT_UPDATE({ guildId, count }) { + onlineMemberMap.set(guildId, count); + } + }); +}); diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index d9cd548e..92e9a205 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -16,101 +16,66 @@ * along with this program. If not, see . */ +import "./style.css"; + +import { definePluginSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Flex } from "@components/Flex"; import { Devs } from "@utils/constants"; -import { getCurrentChannel } from "@utils/discord"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { findStoreLazy } from "@webpack"; -import { SelectedChannelStore, Tooltip, useStateFromStores } from "@webpack/common"; import { FluxStore } from "@webpack/types"; -const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; }; -const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & { +import { MemberCount } from "./MemberCount"; + +export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; }; +export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & { getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; }; }; +const settings = definePluginSettings({ + toolTip: { + type: OptionType.BOOLEAN, + description: "If the member count should be displayed on the server tooltip", + default: true, + restartNeeded: true + }, + memberList: { + type: OptionType.BOOLEAN, + description: "If the member count should be displayed on the member list", + default: true, + restartNeeded: true + } +}); + const sharedIntlNumberFormat = new Intl.NumberFormat(); -const numberFormat = (value: number) => sharedIntlNumberFormat.format(value); - -function MemberCount() { - const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); - const { groups } = useStateFromStores( - [ChannelMemberStore], - () => ChannelMemberStore.getProps(guildId, channelId) - ); - const total = useStateFromStores( - [GuildMemberCountStore], - () => GuildMemberCountStore.getMemberCount(guildId) - ); - - if (total == null) - return null; - - const online = - (groups.length === 1 && groups[0].id === "unknown") - ? 0 - : groups.reduce((count, curr) => count + (curr.id === "offline" ? 0 : curr.count), 0); - - return ( - - - {props => ( -
- - {numberFormat(online)} -
- )} -
- - {props => ( -
- - {numberFormat(total)} -
- )} -
-
- ); -} +export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value); +export const cl = classNameFactory("vc-membercount-"); export default definePlugin({ name: "MemberCount", - description: "Shows the amount of online & total members in the server member list", + description: "Shows the amount of online & total members in the server member list and tooltip", authors: [Devs.Ven, Devs.Commandtechno], + settings, - patches: [{ - find: "{isSidebarVisible:", - replacement: { - match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, - replace: ":[$1?.startsWith('members')?$self.render():null,$2" + patches: [ + { + find: "{isSidebarVisible:", + replacement: { + match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, + replace: ":[$1?.startsWith('members')?$self.render():null,$2" + }, + predicate: () => settings.store.memberList + }, + { + find: ".invitesDisabledTooltip", + replacement: { + match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/, + replace: ",$self.renderTooltip(arguments[0].guild)]" + }, + predicate: () => settings.store.toolTip } - }], - - render: ErrorBoundary.wrap(MemberCount, { noop: true }) + ], + render: ErrorBoundary.wrap(MemberCount, { noop: true }), + renderTooltip: ErrorBoundary.wrap(guild => , { noop: true }) }); diff --git a/src/plugins/memberCount/style.css b/src/plugins/memberCount/style.css new file mode 100644 index 00000000..f43bff83 --- /dev/null +++ b/src/plugins/memberCount/style.css @@ -0,0 +1,44 @@ +.vc-membercount-widget { + display: flex; + align-content: center; + + --color-online: var(--green-360); + --color-total: var(--primary-400); +} + +.vc-membercount-tooltip { + margin-top: 0.25em; + margin-left: 2px; +} + +.vc-membercount-member-list { + justify-content: center; + margin-top: 1em; + padding-inline: 1em; +} + +.vc-membercount-online { + color: var(--color-online); +} + +.vc-membercount-total { + color: var(--color-total); +} + +.vc-membercount-online-dot { + background-color: var(--color-online); + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 0.5em; +} + +.vc-membercount-total-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + border: 3px solid var(--color-total); + margin: 0 0.5em 0 1em; +} diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index ef986bf8..8bc563b1 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -18,7 +18,7 @@ import "./messageLogger.css"; -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -45,7 +45,7 @@ function addDeleteStyle() { const REMOVE_HISTORY_ID = "ml-remove-history"; const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style"; -const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => { +const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => { const { message } = props; const { deleted, editHistory, id, channel_id } = message; @@ -94,13 +94,12 @@ export default definePlugin({ description: "Temporarily logs deleted and edited messages.", authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN], - start() { - addDeleteStyle(); - addContextMenuPatch("message", patchMessageContextMenu); + contextMenus: { + "message": patchMessageContextMenu }, - stop() { - removeContextMenuPatch("message", patchMessageContextMenu); + start() { + addDeleteStyle(); }, renderEdit(edit: { timestamp: any, content: string; }) { diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 40d5201c..f5e4b614 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -47,8 +47,8 @@ export default definePlugin({ { find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded replacement: { - match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/, - replace: '($1||arguments[0].isCurrentUser)?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),' + match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/, + replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),' } }, { diff --git a/src/plugins/notificationVolume/index.ts b/src/plugins/notificationVolume/index.ts index 50eabee7..bc3c7539 100644 --- a/src/plugins/notificationVolume/index.ts +++ b/src/plugins/notificationVolume/index.ts @@ -27,8 +27,8 @@ export default definePlugin({ { find: "_ensureAudio(){", replacement: { - match: /onloadeddata=\(\)=>\{.\.volume=/, - replace: "$&$self.settings.store.notificationVolume/100*" + match: /(?=Math\.min\(\i\.\i\.getOutputVolume\(\)\/100)/, + replace: "$self.settings.store.notificationVolume/100*" }, }, ], diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx index e0d25c7a..adadc90b 100644 --- a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx +++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx @@ -19,7 +19,7 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; -import { getUniqueUsername } from "@utils/discord"; +import { getGuildRoles, getUniqueUsername } from "@utils/discord"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ContextMenuApi, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; import type { Guild } from "discord-types/general"; @@ -78,6 +78,8 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea const [selectedItemIndex, selectItem] = useState(0); const selectedItem = permissions[selectedItemIndex]; + const roles = getGuildRoles(guild.id); + return ( {permissions.map((permission, index) => { const user = UserStore.getUser(permission.id ?? ""); - const role = guild.roles[permission.id ?? ""]; + const role = roles[permission.id ?? ""]; return (
+ } + action={() => openModal(modalProps => )} + /> + ); +}; + export default definePlugin({ name: "VoiceMessages", description: "Allows you to send voice messages like on mobile. To do so, right click the upload button and click Send Voice Message", authors: [Devs.Ven, Devs.Vap, Devs.Nickyux], settings, - - start() { - addContextMenuPatch("channel-attach", ctxMenuPatch); - }, - - stop() { - removeContextMenuPatch("channel-attach", ctxMenuPatch); + contextMenus: { + "channel-attach": ctxMenuPatch } }); @@ -234,20 +246,3 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { ); } - -const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return; - - children.push( - - -
Send voice message
- - } - action={() => openModal(modalProps => )} - /> - ); -}; diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts index 5461696e..3f7eb6e9 100644 --- a/src/plugins/xsOverlay.desktop/index.ts +++ b/src/plugins/xsOverlay.desktop/index.ts @@ -7,6 +7,7 @@ import { definePluginSettings } from "@api/Settings"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; +import { getGuildRoles } from "@utils/discord"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType, PluginNative } from "@utils/types"; import { findByPropsLazy } from "@webpack"; @@ -196,7 +197,7 @@ export default definePlugin({ if (message.mention_roles.length > 0) { for (const roleId of message.mention_roles) { - const role = GuildStore.getGuild(channel.guild_id).roles[roleId]; + const role = getGuildRoles(channel.guild_id)[roleId]; if (!role) continue; const roleColor = role.colorString ?? `#${pingColor}`; finalMsg = finalMsg.replace(`<@&${roleId}>`, `@${role.name}`); diff --git a/src/utils/cloud.tsx b/src/utils/cloud.tsx index f56c78dc..508b1c7e 100644 --- a/src/utils/cloud.tsx +++ b/src/utils/cloud.tsx @@ -106,7 +106,7 @@ export async function authorizeCloud() { try { const res = await fetch(location, { - headers: new Headers({ Accept: "application/json" }) + headers: { Accept: "application/json" } }); const { secret } = await res.json(); if (secret) { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d66bdc82..081eed34 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -418,6 +418,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({ Av32000: { name: "Av32000", id: 593436735380127770n, + }, + Kyuuhachi: { + name: "Kyuuhachi", + id: 236588665420251137n, + }, + Elvyra: { + name: "Elvyra", + id: 708275751816003615n, } } satisfies Record); diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index 74e1aefe..08679db4 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -18,7 +18,7 @@ import { MessageObject } from "@api/MessageEvents"; import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common"; -import { Guild, Message, User } from "discord-types/general"; +import { Guild, Message, Role, User } from "discord-types/general"; import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal"; @@ -185,3 +185,11 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt export function getUniqueUsername(user: User) { return user.discriminator === "0" ? user.username : user.tag; } + +// FIXME: remove this once discord merges the role change into stable +export function getGuildRoles(guildId: string): Record { + if ("getRoles" in GuildStore) + return (GuildStore as any).getRoles(guildId); + + return GuildStore.getGuild(guildId)?.roles ?? {}; +} diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index ec32e2b1..9a0f260a 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -118,10 +118,10 @@ export async function putCloudSettings(manual?: boolean) { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "PUT", - headers: new Headers({ + headers: { Authorization: await getCloudAuth(), "Content-Type": "application/octet-stream" - }), + }, body: deflateSync(new TextEncoder().encode(settings)) }); @@ -162,11 +162,11 @@ export async function getCloudSettings(shouldNotify = true, force = false) { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "GET", - headers: new Headers({ + headers: { Authorization: await getCloudAuth(), Accept: "application/octet-stream", "If-None-Match": Settings.cloud.settingsSyncVersion.toString() - }), + }, }); if (res.status === 404) { @@ -251,9 +251,7 @@ export async function deleteCloudSettings() { try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "DELETE", - headers: new Headers({ - Authorization: await getCloudAuth() - }), + headers: { Authorization: await getCloudAuth() }, }); if (!res.ok) { diff --git a/src/utils/types.ts b/src/utils/types.ts index 16867a43..bec7cb0b 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -17,6 +17,7 @@ */ import { Command } from "@api/Commands"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { FluxEvents } from "@webpack/types"; import { Promisable } from "type-fest"; @@ -115,6 +116,10 @@ export interface PluginDef { flux?: { [E in FluxEvents]?: (event: any) => void; }; + /** + * Allows you to manipulate context menus + */ + contextMenus?: Record; /** * Allows you to add custom actions to the Vencord Toolbox. * The key will be used as text for the button diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index d7bb5d75..048e65d6 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -51,7 +51,7 @@ export let Avatar: t.Avatar; /** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */ export let useToken: t.useToken; -export const MaskedLink = waitForComponent("MaskedLink", m => m?.type?.toString().includes("MASKED_LINK)")); +export const MaskedLink = waitForComponent("MaskedLink", filters.componentByCode("MASKED_LINK)")); export const Timestamp = waitForComponent("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const Flex = waitForComponent("Flex", ["Justify", "Align", "Wrap"]); diff --git a/src/webpack/common/settingsStores.ts b/src/webpack/common/settingsStores.ts index 6db21949..4a48efda 100644 --- a/src/webpack/common/settingsStores.ts +++ b/src/webpack/common/settingsStores.ts @@ -6,7 +6,10 @@ import { findByPropsLazy } from "@webpack"; -export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact"); -export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame"); +import * as t from "./types/settingsStores"; + + +export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact") as Record; +export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame") as Record; export const UserSettingsActionCreators = findByPropsLazy("PreloadedUserSettingsActionCreators"); diff --git a/src/webpack/common/types/index.d.ts b/src/webpack/common/types/index.d.ts index af4b5e1f..01c96855 100644 --- a/src/webpack/common/types/index.d.ts +++ b/src/webpack/common/types/index.d.ts @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +export * from "./classes"; export * from "./components"; export * from "./fluxEvents"; +export * from "./i18nMessages"; export * from "./menu"; +export * from "./settingsStores"; export * from "./stores"; export * from "./utils"; - diff --git a/src/webpack/common/types/settingsStores.ts b/src/webpack/common/types/settingsStores.ts new file mode 100644 index 00000000..5453ca35 --- /dev/null +++ b/src/webpack/common/types/settingsStores.ts @@ -0,0 +1,11 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface SettingsStore { + getSetting(): T; + updateSetting(value: T): void; + useSetting(): T; +} diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index d65f57fc..992bf38f 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -60,6 +60,7 @@ export const filters = { return m => { if (filter(m)) return true; if (!m.$$typeof) return false; + if (m.type && m.type.render) return filter(m.type.render); // memo + forwardRef if (m.type) return filter(m.type); // memos if (m.render) return filter(m.render); // forwardRefs return false; @@ -83,8 +84,8 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { return true; } +let devToolsOpen = false; if (IS_DEV && IS_DISCORD_DESKTOP) { - var devToolsOpen = false; // At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed setTimeout(() => { DiscordNative/* just to make sure */?.window.setDevtoolsCallbacks(() => devToolsOpen = true, () => devToolsOpen = false); @@ -475,8 +476,10 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback else if (typeof filter !== "function") throw new Error("filter must be a string, string[] or function, got " + typeof filter); - const [existing, id] = find(filter!, { isIndirect: true, isWaitFor: true }); - if (existing) return void callback(existing, id); + if (cache != null) { + const [existing, id] = find(filter, { isIndirect: true, isWaitFor: true }); + if (existing) return void callback(existing, id); + } subscriptions.set(filter, callback); }