Vencord/src/api/ContextMenu.ts

182 lines
6.2 KiB
TypeScript
Raw Normal View History

2023-03-08 04:59:50 +00:00
/*
* 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 <https://www.gnu.org/licenses/>.
*/
2023-05-05 23:36:00 +00:00
import { Logger } from "@utils/Logger";
2024-03-07 10:06:24 +00:00
import { Menu, React } from "@webpack/common";
2023-03-08 04:59:50 +00:00
import type { ReactElement } from "react";
/**
* @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
*/
2024-03-07 10:06:24 +00:00
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => void;
2023-03-08 04:59:50 +00:00
/**
* @param navId The navId of the context menu being patched
2023-03-08 04:59:50 +00:00
* @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
*/
2024-03-07 10:06:24 +00:00
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => void;
2023-03-08 04:59:50 +00:00
const ContextMenuLogger = new Logger("ContextMenu");
export const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();
export const globalPatches = new Set<GlobalContextMenuPatchCallback>();
/**
* Add a context menu patch
* @param navId The navId(s) for the context menu(s) to patch
* @param patch The patch to be applied
*/
export function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {
if (!Array.isArray(navId)) navId = [navId];
for (const id of navId) {
let contextMenuPatches = navPatches.get(id);
if (!contextMenuPatches) {
contextMenuPatches = new Set();
navPatches.set(id, contextMenuPatches);
}
contextMenuPatches.add(patch);
}
}
/**
* Add a global context menu patch that fires the patch for all context menus
* @param patch The patch to be applied
*/
export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {
globalPatches.add(patch);
}
/**
* Remove a context menu patch
* @param navId The navId(s) for the context menu(s) to remove the patch
* @param patch The patch to be removed
2023-10-27 02:06:05 +00:00
* @returns Whether the patch was successfully removed from the context menu(s)
2023-03-08 04:59:50 +00:00
*/
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
const navIds = Array.isArray(navId) ? navId : [navId as string];
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;
}
/**
* Remove a global context menu patch
* @param patch The patch to be removed
2023-10-27 02:06:05 +00:00
* @returns Whether the patch was successfully removed
2023-03-08 04:59:50 +00:00
*/
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
return globalPatches.delete(patch);
}
/**
* A helper function for finding the children array of a group nested inside a context menu based on the id(s) of its children
* @param id The id of the child. If an array is specified, all ids will be tried
* @param children The context menu children
2023-03-08 04:59:50 +00:00
*/
2024-03-07 10:06:24 +00:00
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>): Array<ReactElement | null> | null {
2023-03-08 04:59:50 +00:00
for (const child of children) {
if (child == null) continue;
2023-03-08 04:59:50 +00:00
2024-03-07 10:06:24 +00:00
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
2024-03-07 10:06:24 +00:00
) return children;
2023-03-08 04:59:50 +00:00
let nextChildren = child.props?.children;
if (nextChildren) {
if (!Array.isArray(nextChildren)) {
nextChildren = [nextChildren];
child.props.children = nextChildren;
}
2024-03-07 10:06:24 +00:00
const found = findGroupChildrenByChildId(id, nextChildren);
2023-03-08 04:59:50 +00:00
if (found !== null) return found;
}
}
return null;
}
interface ContextMenuProps {
contextMenuApiArguments?: Array<any>;
navId: string;
children: Array<ReactElement | null>;
2023-03-08 04:59:50 +00:00
"aria-label": string;
onSelect: (() => void) | undefined;
onClose: (callback: (...args: Array<any>) => any) => void;
}
2024-03-07 10:06:24 +00:00
export function _usePatchContextMenu(props: ContextMenuProps) {
props = {
...props,
children: cloneMenuChildren(props.children),
};
2023-03-19 07:53:00 +00:00
props.contextMenuApiArguments ??= [];
2023-03-08 04:59:50 +00:00
const contextMenuPatches = navPatches.get(props.navId);
if (!Array.isArray(props.children)) props.children = [props.children];
2023-03-08 04:59:50 +00:00
if (contextMenuPatches) {
for (const patch of contextMenuPatches) {
try {
2024-03-07 10:06:24 +00:00
patch(props.children, ...props.contextMenuApiArguments);
2023-03-08 04:59:50 +00:00
} catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
}
}
}
for (const patch of globalPatches) {
try {
2024-03-07 10:06:24 +00:00
patch(props.navId, props.children, ...props.contextMenuApiArguments);
2023-03-08 04:59:50 +00:00
} catch (err) {
ContextMenuLogger.error("Global patch errored,", err);
}
}
2024-03-07 10:06:24 +00:00
return props;
}
function cloneMenuChildren(obj: ReactElement | Array<ReactElement | null> | 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;
2023-03-08 04:59:50 +00:00
}