diff --git a/src/plugins/welcomeStickerPicker.tsx b/src/plugins/welcomeStickerPicker.tsx new file mode 100644 index 000000000..40af7e2a0 --- /dev/null +++ b/src/plugins/welcomeStickerPicker.tsx @@ -0,0 +1,185 @@ +/* + * 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 } from "@api/settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { ContextMenu, FluxDispatcher, Menu } from "@webpack/common"; +import { Channel, Message } from "discord-types/general"; + +interface Sticker { + id: string; + format_type: number; + description: string; + name: string; +} + +enum GreetMode { + Greet = "Greet", + NormalMessage = "Message" +} + +const settings = definePluginSettings({ + greetMode: { + type: OptionType.SELECT, + options: [ + { label: "Greet (you can only greet 3 times)", value: GreetMode.Greet, default: true }, + { label: "Normal Message (you can greet spam)", value: GreetMode.NormalMessage } + ], + description: "Choose the greet mode" + } +}); + +const MessageActions = findByPropsLazy("sendGreetMessage"); + +function greet(channel: Channel, message: Message, stickers: string[]) { + const options = MessageActions.getSendMessageOptionsForReply({ + channel, + message, + shouldMention: true, + showMentionToggle: true + }); + + if (settings.store.greetMode === GreetMode.NormalMessage || stickers.length > 1) { + options.stickerIds = stickers; + const msg = { + content: "", + tts: false, + invalidEmojis: [], + validNonShortcutEmojis: [] + }; + + MessageActions._sendMessage(channel.id, msg, options); + } else { + MessageActions.sendGreetMessage(channel.id, stickers[0], options); + } +} + + +function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], message: Message, channel: Channel; }) { + const s = settings.use(["greetMode", "multiGreetChoices"] as any) as { greetMode: GreetMode, multiGreetChoices: string[]; }; + const { greetMode, multiGreetChoices = [] } = s; + + return ( + FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} + aria-label="Greet Sticker Picker" + > + + {Object.values(GreetMode).map(mode => ( + s.greetMode = mode} + /> + ))} + + + + + + {stickers.map(sticker => ( + greet(channel, message, [sticker.id])} + /> + ))} + + + {!(settings.store as any).unholyMultiGreetEnabled ? null : ( + <> + + + + {stickers.map(sticker => { + const checked = multiGreetChoices.some(s => s === sticker.id); + + return ( + = 3} + action={() => { + s.multiGreetChoices = checked + ? multiGreetChoices.filter(s => s !== sticker.id) + : [...multiGreetChoices, sticker.id]; + }} + /> + ); + })} + + + greet(channel, message, multiGreetChoices!)} + disabled={multiGreetChoices.length === 0} + /> + + + + )} + + ); +} + +export default definePlugin({ + name: "GreetStickerPicker", + description: "Allows you to use any greet sticker instead of only the random one by right-clicking the 'Wave to say hi!' button", + authors: [Devs.Ven], + + settings, + + patches: [ + { + find: "Messages.WELCOME_CTA_LABEL", + replacement: { + match: /innerClassName:\i\(\).welcomeCTAButton,(?<=%\i\.length;return (\i)\[\i\].+?)/, + replace: "$&onContextMenu:(e)=>$self.pickSticker(e,$1,arguments[0])," + } + } + ], + + pickSticker( + event: React.UIEvent, + stickers: Sticker[], + props: { + channel: Channel, + message: Message; + } + ) { + if (!(props.message as any).deleted) + ContextMenu.open(event, () => ); + } +}); diff --git a/src/webpack/common/types/menu.d.ts b/src/webpack/common/types/menu.d.ts index bf5508ae8..b52e78fdb 100644 --- a/src/webpack/common/types/menu.d.ts +++ b/src/webpack/common/types/menu.d.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import type { ComponentType, CSSProperties, PropsWithChildren, UIEvent } from "react"; +import type { ComponentType, CSSProperties, MouseEvent, PropsWithChildren, UIEvent } from "react"; type RC = ComponentType>>; @@ -30,10 +30,14 @@ export interface Menu { onSelect?(): void; }>; MenuSeparator: ComponentType; - MenuGroup: RC; + MenuGroup: RC<{ + label?: string; + }>; MenuItem: RC<{ id: string; label: string; + action?(e: MouseEvent): void; + render?: ComponentType; onChildrenScroll?: Function; childRowHeight?: number; @@ -41,9 +45,18 @@ export interface Menu { }>; MenuCheckboxItem: RC<{ id: string; + label: string; + checked: boolean; + action?(e: MouseEvent): void; + disabled?: boolean; }>; MenuRadioItem: RC<{ id: string; + group: string; + label: string; + checked: boolean; + action?(e: MouseEvent): void; + disabled?: boolean; }>; MenuControlItem: RC<{ id: string;