From 727297ec4efc2b43a953a68bfed0efe6c6fffaf3 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 2 Mar 2023 18:49:09 +0100 Subject: [PATCH 001/147] Fix messageLinkEmbeds --- src/plugins/messageLinkEmbeds.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/messageLinkEmbeds.tsx b/src/plugins/messageLinkEmbeds.tsx index a6b63d82c..75b99e149 100644 --- a/src/plugins/messageLinkEmbeds.tsx +++ b/src/plugins/messageLinkEmbeds.tsx @@ -158,11 +158,11 @@ export default definePlugin({ { find: ".embedCard", replacement: [{ - match: /{"use strict";(.{0,10})\(\)=>(.{1,2})}\);/, - replace: '{"use strict";$1()=>$2,me:()=>messageEmbed});' + match: /{"use strict";(.{0,10})\(\)=>(\i)}\);/, + replace: '{"use strict";$1()=>$2,me:()=>typeof messageEmbed !== "undefined" ? messageEmbed : null});' }, { - match: /function (.{1,2})\(.{1,2}\){var .{1,2}=.{1,2}\.message,.{1,2}=.{1,2}\.channel.{0,300}\.embedCard.{0,500}}\)}/, - replace: "$&;var messageEmbed={mle_AutomodEmbed:$1};" + match: /function (\i)\(\i\){var \i=\i\.message,\i=\i\.channel.{0,200}\.hideTimestamp/, + replace: "var messageEmbed={mle_AutomodEmbed:$1};$&" }] } ], From d6a3edefd9f4cf71fcd96cbc44c55bfc31b088a6 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 2 Mar 2023 21:01:31 +0100 Subject: [PATCH 002/147] Rewrite MessageLinkEmbeds to improve Code Quality --- src/plugins/messageLinkEmbeds.tsx | 527 ++++++++++++++++-------------- 1 file changed, 283 insertions(+), 244 deletions(-) diff --git a/src/plugins/messageLinkEmbeds.tsx b/src/plugins/messageLinkEmbeds.tsx index 75b99e149..e7b3f7217 100644 --- a/src/plugins/messageLinkEmbeds.tsx +++ b/src/plugins/messageLinkEmbeds.tsx @@ -17,11 +17,13 @@ */ import { addAccessory } from "@api/MessageAccessories"; -import { Settings } from "@api/settings"; +import { definePluginSettings } from "@api/settings"; +import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants.js"; +import { classes, LazyComponent } from "@utils/misc"; import { Queue } from "@utils/Queue"; import definePlugin, { OptionType } from "@utils/types"; -import { filters, findByPropsLazy, waitFor } from "@webpack"; +import { find, findByCode, findByPropsLazy } from "@webpack"; import { Button, ChannelStore, @@ -36,41 +38,20 @@ import { } from "@webpack/common"; import { Channel, Guild, Message } from "discord-types/general"; -let messageCache: { [id: string]: { message?: Message, fetched: boolean; }; } = {}; +const messageCache = new Map(); -let AutomodEmbed: React.ComponentType, - Embed: React.ComponentType, - ChannelMessage: React.ComponentType, - Endpoints: Record; +const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed")); +const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes('["message","compact","className",'))); -waitFor(["mle_AutomodEmbed"], m => (AutomodEmbed = m.mle_AutomodEmbed)); -waitFor(filters.byCode(".inlineMediaEmbed"), m => Embed = m); -waitFor(m => m.type?.toString()?.includes('["message","compact","className",'), m => ChannelMessage = m); -waitFor(["MESSAGE_CREATE_ATTACHMENT_UPLOAD"], _ => Endpoints = _); const SearchResultClasses = findByPropsLazy("message", "searchResult"); -const messageFetchQueue = new Queue(); -async function fetchMessage(channelID: string, messageID: string): Promise { - if (messageID in messageCache && !messageCache[messageID].fetched) return Promise.resolve(); - if (messageCache[messageID]?.fetched) return Promise.resolve(messageCache[messageID].message); +let AutoModEmbed: React.ComponentType = () => null; - messageCache[messageID] = { fetched: false }; - const res = await RestAPI.get({ - url: Endpoints.MESSAGES(channelID), - query: { - limit: 1, - around: messageID - }, - retries: 2 - }).catch(() => { }); - const apiMessage = res.body?.[0]; - const message: Message = MessageStore.getMessages(apiMessage.channel_id).receiveMessage(apiMessage).get(apiMessage.id); - messageCache[message.id] = { - message: message, - fetched: true - }; - return Promise.resolve(message); -} +const messageLinkRegex = /(? { - if (a.content_type!.startsWith("image/")) attachments.push({ - height: a.height!, - width: a.width!, - url: a.url, - proxyURL: a.proxy_url! - }); - }); - message.embeds?.forEach(e => { - if (e.type === "image") attachments.push( - e.image ? { ...e.image } : { ...e.thumbnail! } - ); - if (e.type === "gifv" && !isTenorGif.test(e.url!)) { - attachments.push({ - height: e.thumbnail!.height, - width: e.thumbnail!.width, - url: e.url! - }); - } - }); - return attachments; -} - -const noContent = (attachments: number, embeds: number): string => { - if (!attachments && !embeds) return ""; - if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`; - if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`; - return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`; -}; - -function requiresRichEmbed(message: Message) { - if (message.attachments.every(a => a.content_type?.startsWith("image/")) - && message.embeds.every(e => e.type === "image" || (e.type === "gifv" && !isTenorGif.test(e.url!))) - && !message.components.length - ) return false; - return true; -} - -const computeWidthAndHeight = (width: number, height: number) => { - const maxWidth = 400, maxHeight = 300; - let newWidth: number, newHeight: number; - if (width > height) { - newWidth = Math.min(width, maxWidth); - newHeight = Math.round(height / (width / newWidth)); - } else { - newHeight = Math.min(height, maxHeight); - newWidth = Math.round(width / (height / newHeight)); - } - return { width: newWidth, height: newHeight }; -}; - interface MessageEmbedProps { message: Message; channel: Channel; guildID: string; } +const messageFetchQueue = new Queue(); + +const settings = definePluginSettings({ + messageBackgroundColor: { + description: "Background color for messages in rich embeds", + type: OptionType.BOOLEAN + }, + automodEmbeds: { + description: "Use automod embeds instead of rich embeds (smaller but less info)", + type: OptionType.SELECT, + options: [ + { + label: "Always use automod embeds", + value: "always" + }, + { + label: "Prefer automod embeds, but use rich embeds if some content can't be shown", + value: "prefer" + }, + { + label: "Never use automod embeds", + value: "never", + default: true + } + ] + }, + clearMessageCache: { + type: OptionType.COMPONENT, + description: "Clear the linked message cache", + component: () => + + } +}); + + +async function fetchMessage(channelID: string, messageID: string) { + const cached = messageCache.get(messageID); + if (cached) return cached.message; + + messageCache.set(messageID, { fetched: false }); + + const res = await RestAPI.get({ + url: `/channels/${channelID}/messages`, + query: { + limit: 1, + around: messageID + }, + retries: 2 + }).catch(() => null); + + const msg = res?.body?.[0]; + if (!msg) return; + + const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id); + + messageCache.set(message.id, { + message, + fetched: true + }); + + return message; +} + + +function getImages(message: Message): Attachment[] { + const attachments: Attachment[] = []; + + for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) { + if (content_type?.startsWith("image/")) + attachments.push({ + height: height!, + width: width!, + url: url, + proxyURL: proxy_url! + }); + } + + for (const { type, image, thumbnail, url } of message.embeds ?? []) { + if (type === "image") + attachments.push({ ...(image ?? thumbnail!) }); + else if (url && type === "gifv" && !tenorRegex.test(url)) + attachments.push({ + height: thumbnail!.height, + width: thumbnail!.width, + url + }); + } + + return attachments; +} + +function noContent(attachments: number, embeds: number) { + if (!attachments && !embeds) return ""; + if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`; + if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`; + return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`; +} + +function requiresRichEmbed(message: Message) { + if (message.components.length) return true; + if (message.attachments.some(a => a.content_type?.startsWith("image/"))) return true; + if (message.embeds.some(e => e.type === "image" || (e.type === "gifv" && !tenorRegex.test(e.url!)))) return true; + + return false; +} + +function computeWidthAndHeight(width: number, height: number) { + const maxWidth = 400; + const maxHeight = 300; + + if (width > height) { + const adjustedWidth = Math.min(width, maxWidth); + return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) }; + } + + const adjustedHeight = Math.min(height, maxHeight); + return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight }; +} + function withEmbeddedBy(message: Message, embeddedBy: string[]) { return new Proxy(message, { get(_, prop) { @@ -149,181 +197,172 @@ function withEmbeddedBy(message: Message, embeddedBy: string[]) { }); } + +function MessageEmbedAccessory({ message }: { message: Message; }) { + // @ts-ignore + const embeddedBy: string[] = message.vencordEmbeddedBy ?? []; + + const accessories = [] as (JSX.Element | null)[]; + + let match = null as RegExpMatchArray | null; + while ((match = messageLinkRegex.exec(message.content!)) !== null) { + const [_, guildID, channelID, messageID] = match; + if (embeddedBy.includes(messageID)) { + continue; + } + + const linkedChannel = ChannelStore.getChannel(channelID); + if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) { + continue; + } + + let linkedMessage = messageCache.get(messageID)?.message; + if (!linkedMessage) { + linkedMessage ??= MessageStore.getMessage(channelID, messageID); + if (linkedMessage) { + messageCache.set(messageID, { message: linkedMessage, fetched: true }); + } else { + const msg = { ...message } as any; + delete msg.embeds; + messageFetchQueue.push(() => fetchMessage(channelID, messageID) + .then(m => m && FluxDispatcher.dispatch({ + type: "MESSAGE_UPDATE", + message: msg + })) + ); + continue; + } + } + + const messageProps: MessageEmbedProps = { + message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]), + channel: linkedChannel, + guildID + }; + + const type = settings.store.automodEmbeds; + accessories.push( + type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage)) + ? + : + ); + } + + return accessories.length ? <>{accessories} : null; +} + +function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null { + const isDM = guildID === "@me"; + + const guild = !isDM && GuildStore.getGuild(channel.guild_id); + const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]); + + + return + {isDM ? "Direct Message - " : (guild as Guild).name + " - "}, + {isDM + ? Parser.parse(`<@${dmReceiver.id}>`) + : Parser.parse(`<#${channel.id}>`) + } + , + iconProxyURL: guild + ? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png` + : `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}` + } + }} + renderDescription={() => ( +
+ +
+ )} + />; +} + +function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { + const { message, channel, guildID } = props; + + const isDM = guildID === "@me"; + const images = getImages(message); + const { parse } = Parser; + + return + {isDM + ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) + : parse(`<#${channel.id}>`) + }, + {isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name} + + } + compact={false} + content={ + <> + {message.content || message.attachments.length <= images.length + ? parse(message.content) + : [noContent(message.attachments.length, message.embeds.length)] + } + {images.map(a => { + const { width, height } = computeWidthAndHeight(a.width, a.height); + return ( +
+ +
+ ); + })} + + } + hideTimestamp={false} + message={message} + _messageEmbed="automod" + />; +} + export default definePlugin({ name: "MessageLinkEmbeds", description: "Adds a preview to messages that link another message", - authors: [Devs.TheSun], + authors: [Devs.TheSun, Devs.Ven], dependencies: ["MessageAccessoriesAPI"], patches: [ { find: ".embedCard", replacement: [{ - match: /{"use strict";(.{0,10})\(\)=>(\i)}\);/, - replace: '{"use strict";$1()=>$2,me:()=>typeof messageEmbed !== "undefined" ? messageEmbed : null});' - }, { match: /function (\i)\(\i\){var \i=\i\.message,\i=\i\.channel.{0,200}\.hideTimestamp/, - replace: "var messageEmbed={mle_AutomodEmbed:$1};$&" + replace: "$self.AutoModEmbed=$1;$&" }] } ], - options: { - messageBackgroundColor: { - description: "Background color for messages in rich embeds", - type: OptionType.BOOLEAN - }, - automodEmbeds: { - description: "Use automod embeds instead of rich embeds (smaller but less info)", - type: OptionType.SELECT, - options: [{ - label: "Always use automod embeds", - value: "always" - }, { - label: "Prefer automod embeds, but use rich embeds if some content can't be shown", - value: "prefer" - }, { - label: "Never use automod embeds", - value: "never", - default: true - }] - }, - clearMessageCache: { - type: OptionType.COMPONENT, - description: "Clear the linked message cache", - component: () => - - } + + set AutoModEmbed(e: any) { + AutoModEmbed = e; }, + settings, + start() { - addAccessory("messageLinkEmbed", props => this.messageEmbedAccessory(props), 4 /* just above rich embeds*/); - }, + addAccessory("messageLinkEmbed", props => { + if (!messageLinkRegex.test(props.message.content)) + return null; - messageLinkRegex: /(? fetchMessage(channelID, messageID) - .then(m => m && FluxDispatcher.dispatch({ - type: "MESSAGE_UPDATE", - message: msg - })) - ); - continue; - } - } - const messageProps: MessageEmbedProps = { - message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]), - channel: linkedChannel, - guildID - }; - - const type = Settings.plugins[this.name].automodEmbeds; - accessories.push( - type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage)) - ? this.automodEmbedAccessory(messageProps) - : this.channelMessageEmbedAccessory(messageProps) + return ( + + + ); - } - return accessories; - }, - - channelMessageEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { - const { message, channel, guildID } = props; - - const isDM = guildID === "@me"; - const guild = !isDM && GuildStore.getGuild(channel.guild_id); - const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]); - const classNames = [SearchResultClasses.message]; - if (Settings.plugins[this.name].messageBackgroundColor) classNames.push(SearchResultClasses.searchResult); - - return - {[ - {isDM ? "Direct Message - " : (guild as Guild).name + " - "}, - ...(isDM - ? Parser.parse(`<@${dmReceiver.id}>`) - : Parser.parse(`<#${channel.id}>`) - ) - ]} - , - iconProxyURL: guild - ? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png` - : `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}` - } - }} - renderDescription={() => { - return
- -
; - }} - />; - }, - - automodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { - const { message, channel, guildID } = props; - - const isDM = guildID === "@me"; - const images = getImages(message); - const { parse } = Parser; - - return - {[ - ...(isDM ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) : parse(`<#${channel.id}>`)), - {isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name} - ]} - } - compact={false} - content={[ - ...(message.content || !(message.attachments.length > images.length) - ? parse(message.content) - : [noContent(message.attachments.length, message.embeds.length)] - ), - ...(images.map(a => { - const { width, height } = computeWidthAndHeight(a.width, a.height); - return
; - } - )) - ]} - hideTimestamp={false} - message={message} - _messageEmbed="automod" - />; + }, 4 /* just above rich embeds */); }, }); From ab8c93fbacb78d9358dfc5e8f5b2deb75e12d283 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 2 Mar 2023 21:05:09 +0100 Subject: [PATCH 003/147] Rewrite MessageLinkEmbeds part 2 --- src/plugins/messageLinkEmbeds.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/messageLinkEmbeds.tsx b/src/plugins/messageLinkEmbeds.tsx index e7b3f7217..7ca579410 100644 --- a/src/plugins/messageLinkEmbeds.tsx +++ b/src/plugins/messageLinkEmbeds.tsx @@ -168,8 +168,8 @@ function noContent(attachments: number, embeds: number) { function requiresRichEmbed(message: Message) { if (message.components.length) return true; - if (message.attachments.some(a => a.content_type?.startsWith("image/"))) return true; - if (message.embeds.some(e => e.type === "image" || (e.type === "gifv" && !tenorRegex.test(e.url!)))) return true; + if (message.attachments.some(a => !a.content_type?.startsWith("image/"))) return true; + if (message.embeds.some(e => e.type !== "image" && (e.type !== "gifv" || tenorRegex.test(e.url!)))) return true; return false; } From 5e2ec368ad6136b8b6e4d71011e1d4187ea81aa8 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 2 Mar 2023 21:16:03 +0100 Subject: [PATCH 004/147] patches: Make $self more robust --- src/utils/patches.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/patches.ts b/src/utils/patches.ts index 8ecd68e80..0f83d4003 100644 --- a/src/utils/patches.ts +++ b/src/utils/patches.ts @@ -27,9 +27,13 @@ export function canonicalizeMatch(match: RegExp | string) { return new RegExp(canonSource, match.flags); } -export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string) { - if (typeof replace === "function") return replace; - return replace.replaceAll("$self", `Vencord.Plugins.plugins.${pluginName}`); +export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string): string | ReplaceFn { + const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`; + + if (typeof replace !== "function") + return replace.replaceAll("$self", self); + + return (...args) => replace(...args).replaceAll("$self", self); } export function canonicalizeDescriptor(descriptor: TypedPropertyDescriptor, canonicalize: (value: T) => T) { From 03915b7533b1548a971a0aca7c862dc7fcae4f0d Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 2 Mar 2023 21:19:33 +0100 Subject: [PATCH 005/147] Bump to v1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b077380d1..97ce55c4e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.0.9", + "version": "1.1.0", "description": "The cutest Discord client mod", "keywords": [], "homepage": "https://github.com/Vendicated/Vencord#readme", From 6747276a875567e90be059da63ff1734a9fd4910 Mon Sep 17 00:00:00 2001 From: megumin Date: Fri, 3 Mar 2023 23:07:48 +0000 Subject: [PATCH 006/147] Add admin warnings to INSTALLING.md (#561) --- docs/1_INSTALLING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/1_INSTALLING.md b/docs/1_INSTALLING.md index 96f1bed56..643a1e7a4 100644 --- a/docs/1_INSTALLING.md +++ b/docs/1_INSTALLING.md @@ -31,12 +31,14 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to Install `pnpm`: -> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal. +> :exclamation: This next command may need to be run as admin/sudo depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH. ```shell npm i -g pnpm ``` +> :exclamation: **IMPORTANT** Make sure you aren't using an admin/sudo terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall. + Clone Vencord: ```shell From 92372bde1d422e2daf2e24dd05ae5f4d73f5ebcb Mon Sep 17 00:00:00 2001 From: Berlin <39058370+somerandomcloud@users.noreply.github.com> Date: Sat, 4 Mar 2023 00:55:21 +0100 Subject: [PATCH 007/147] Update 1_INSTALLING.md (#562) --- docs/1_INSTALLING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/1_INSTALLING.md b/docs/1_INSTALLING.md index 643a1e7a4..7fcd57abd 100644 --- a/docs/1_INSTALLING.md +++ b/docs/1_INSTALLING.md @@ -37,7 +37,7 @@ Install `pnpm`: npm i -g pnpm ``` -> :exclamation: **IMPORTANT** Make sure you aren't using an admin/sudo terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall. +> :exclamation: **IMPORTANT** Make sure you aren't using an admin/root terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall. Clone Vencord: From cab72e1be627fb7cb06725d0a3e262e214de170f Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 4 Mar 2023 18:40:37 +0100 Subject: [PATCH 008/147] Strongly type useSettings (supersedes #559) --- src/api/settings.ts | 18 +++++++++++++++--- src/components/PluginSettings/index.tsx | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/api/settings.ts b/src/api/settings.ts index c551df03b..1ca26116b 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -167,11 +167,11 @@ export const Settings = makeProxy(settings); * @returns Settings */ // TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later -export function useSettings(paths?: string[]) { +export function useSettings(paths?: UseSettings[]) { const [, forceUpdate] = React.useReducer(() => ({}), {}); const onUpdate: SubscriptionCallback = paths - ? (value, path) => paths.includes(path) && forceUpdate() + ? (value, path) => paths.includes(path as UseSettings) && forceUpdate() : forceUpdate; React.useEffect(() => { @@ -229,7 +229,7 @@ export function definePluginSettings useSettings( - settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) + settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings[] ).plugins[definedSettings.pluginName] as any, def, checks: checks ?? {}, @@ -237,3 +237,15 @@ export function definePluginSettings = ResolveUseSettings[keyof T]; + +type ResolveUseSettings = { + [Key in keyof T]: + Key extends string + ? T[Key] extends Record + // @ts-ignore "Type instantiation is excessively deep and possibly infinite" + ? UseSettings extends string ? `${Key}.${UseSettings}` : never + : Key + : never; +}; diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 3d69a607d..02d89f81f 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -93,7 +93,7 @@ interface PluginCardProps extends React.HTMLProps { } function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) { - const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name]; + const settings = useSettings([`plugins.${plugin.name}.enabled`]).plugins[plugin.name]; const isEnabled = () => settings.enabled ?? false; From e219aaa062563202c6b723ada06f115437ab35a7 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 4 Mar 2023 14:49:15 -0300 Subject: [PATCH 009/147] Notifications: Permanent option and close button (#563) Co-authored-by: Ven --- .../Notifications/NotificationComponent.tsx | 33 ++++++++++++++++--- src/api/Notifications/Notifications.tsx | 2 ++ src/api/Notifications/styles.css | 33 ++++++++++++++++--- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx index 036cacd00..29cd68f35 100644 --- a/src/api/Notifications/NotificationComponent.tsx +++ b/src/api/Notifications/NotificationComponent.tsx @@ -20,7 +20,7 @@ import "./styles.css"; import { useSettings } from "@api/settings"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common"; +import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common"; import { NotificationData } from "./Notifications"; @@ -32,7 +32,8 @@ export default ErrorBoundary.wrap(function NotificationComponent({ icon, onClick, onClose, - image + image, + permanent }: NotificationData) { const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications; const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused()); @@ -43,7 +44,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({ const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]); useEffect(() => { - if (isHover || !hasFocus || timeout === 0) return void setElapsed(0); + if (isHover || !hasFocus || timeout === 0 || permanent) return void setElapsed(0); const intervalId = setInterval(() => { const elapsed = Date.now() - start; @@ -74,14 +75,36 @@ export default ErrorBoundary.wrap(function NotificationComponent({
{icon && }
- {title} +
+

{title}

+ +
{richBody ??

{body}

}
{image && } - {timeout !== 0 && ( + {timeout !== 0 && !permanent && (
Date: Sun, 5 Mar 2023 22:05:46 +0100 Subject: [PATCH 010/147] Fix(InvisibleChat) Fix chatbar icon patch (closes #560) (#566) Co-authored-by: Ven --- src/plugins/invisibleChat/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/invisibleChat/index.tsx b/src/plugins/invisibleChat/index.tsx index a83d47b84..d3358be43 100644 --- a/src/plugins/invisibleChat/index.tsx +++ b/src/plugins/invisibleChat/index.tsx @@ -91,8 +91,8 @@ function ChatBarIcon() { @@ -131,8 +131,8 @@ export default definePlugin({ { find: ".activeCommandOption", replacement: { - match: /.=.\.activeCommand,.=.\.activeCommandOption,.{1,133}(.)=\[\];/, - replace: "$&;$1.push($self.chatBarIcon());", + match: /(.)\.push.{1,50}\(\i,\{.{1,30}\},"gift"\)\)/, + replace: "$&;try{$1.push($self.chatBarIcon())}catch{}", } }, ], From a5392e5c53675e5206a4d5fcf97b9d6a8d07727b Mon Sep 17 00:00:00 2001 From: Nico Date: Sun, 5 Mar 2023 22:30:37 +0100 Subject: [PATCH 011/147] fix(silentTyping): fix chatbar icon patch (#570) --- src/plugins/silentTyping.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/silentTyping.tsx b/src/plugins/silentTyping.tsx index 78bb92ea0..83f64153f 100644 --- a/src/plugins/silentTyping.tsx +++ b/src/plugins/silentTyping.tsx @@ -82,8 +82,8 @@ export default definePlugin({ find: ".activeCommandOption", predicate: () => settings.store.showIcon, replacement: { - match: /\i=\i\.activeCommand,\i=\i\.activeCommandOption,.{1,133}(.)=\[\];/, - replace: "$&;$1.push($self.chatBarIcon());", + match: /(.)\.push.{1,50}\(\i,\{.{1,30}\},"gift"\)\)/, + replace: "$&;try{$1.push($self.chatBarIcon())}catch{}", } }, ], From bed5e98bb03a2c7073f9f421a396fa7616d56ac5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 5 Mar 2023 18:49:59 -0300 Subject: [PATCH 012/147] Misc fixes and improvements (#555) Co-authored-by: Ven --- src/plugins/fakeNitro.ts | 6 +++--- src/plugins/noScreensharePreview.ts | 6 +++--- .../components/HiddenChannelLockScreen.tsx | 18 ++++++++++++++---- src/plugins/showHiddenChannels/index.tsx | 13 ++++++++----- src/plugins/typingTweaks.tsx | 2 +- src/plugins/webhookTags.ts | 4 ++-- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/plugins/fakeNitro.ts b/src/plugins/fakeNitro.ts index 22ffb8009..a09a95a39 100644 --- a/src/plugins/fakeNitro.ts +++ b/src/plugins/fakeNitro.ts @@ -87,11 +87,11 @@ export default definePlugin({ }, { match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g, - replace: ",fakeNitroIntention" + replace: ',typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0' }, { match: /(?<=&&!\i&&)!(?\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, - replace: `(!$&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))` + replace: `(!$&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))` } ] }, @@ -100,7 +100,7 @@ export default definePlugin({ predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true, replacement: { match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?\i))\){(?.+?\))/g, - replace: `,fakeNitroIntention){$||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` + replace: `,fakeNitroIntention){$||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` } }, { diff --git a/src/plugins/noScreensharePreview.ts b/src/plugins/noScreensharePreview.ts index 5641ea3d7..b09f20636 100644 --- a/src/plugins/noScreensharePreview.ts +++ b/src/plugins/noScreensharePreview.ts @@ -27,10 +27,10 @@ export default definePlugin({ { find: '("ApplicationStreamPreviewUploadManager")', replacement: [ - ".\\.default\\.makeChunkedRequest\\(", - ".{1,2}\\..\\.post\\({url:" + "\\i\\.default\\.makeChunkedRequest\\(", + "\\i\\.\\i\\.post\\({url:" ].map(match => ({ - match: new RegExp(`return\\[(?\\d),${match}.\\..{1,3}\\.STREAM_PREVIEW.+?}\\)\\];`), + match: new RegExp(`return\\[(?\\d),${match}\\i\\.\\i\\.STREAM_PREVIEW.+?}\\)\\];`), replace: 'return[$,Promise.resolve({body:"",status:204})];' })) }, diff --git a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx index 7e66a6a1c..01bc3a7c6 100644 --- a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx +++ b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx @@ -19,9 +19,11 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { LazyComponent } from "@utils/misc"; import { formatDuration } from "@utils/text"; -import { find, findByCode, findByPropsLazy } from "@webpack"; +import { find, findByPropsLazy } from "@webpack"; import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common"; import { Channel } from "discord-types/general"; +import type { ComponentType } from "react"; + enum SortOrderTypes { LATEST_ACTIVITY = 0, @@ -73,6 +75,17 @@ enum ChannelFlags { REQUIRE_TAG = 1 << 4 } +let EmojiComponent: ComponentType; +let ChannelBeginHeader: ComponentType; + +export function setEmojiComponent(component: ComponentType) { + EmojiComponent = component; +} + +export function setChannelBeginHeaderComponent(component: ComponentType) { + ChannelBeginHeader = component; +} + const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase"); const TagComponent = LazyComponent(() => find(m => { if (typeof m !== "function") return false; @@ -81,9 +94,6 @@ const TagComponent = LazyComponent(() => find(m => { // Get the component which doesn't include increasedActivity logic return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill"); })); -const EmojiComponent = LazyComponent(() => findByCode('.jumboable?"jumbo":"default"')); -// The component for the beggining of a channel, but we patched it so it only returns the allowed users and roles components for hidden channels -const ChannelBeginHeader = LazyComponent(() => findByCode(".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE")); const ChannelTypesToChannelNames = { [ChannelTypes.GUILD_TEXT]: "text", diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index fe14fe68b..60e0c51fa 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -26,7 +26,7 @@ import { findByPropsLazy } from "@webpack"; import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common"; import { Channel } from "discord-types/general"; -import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen"; +import HiddenChannelLockScreen, { setChannelBeginHeaderComponent, setEmojiComponent } from "./components/HiddenChannelLockScreen"; const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer"); @@ -239,8 +239,8 @@ export default definePlugin({ { find: 'jumboable?"jumbo":"default"', replacement: { - match: /(?<=\i:\(\)=>\i)(?=}.+?(?\i)=function.{1,20}node,\i=\i.isInteracting)/, - replace: ",hc1:()=>$" // Blame Ven length check for the small name :pensive_cry: + match: /(?<=(?\i)=function.{1,20}node,\i=\i.isInteracting.+?}}\)},)/, + replace: "shcEmojiComponentExport=($self.setEmojiComponent($),void 0)," } }, { @@ -248,8 +248,8 @@ export default definePlugin({ replacement: [ { // Export the channel beggining header - match: /(?<=\i:\(\)=>\i)(?=}.+?function (?\i).{1,600}computePermissionsForRoles)/, - replace: ",hc2:()=>$" + match: /(?<=function (?\i)\(.{1,600}computePermissionsForRoles.+?}\)})(?=var)/, + replace: "$self.setChannelBeginHeaderComponent($);" }, { // Patch the header to only return allowed users and roles if it's a hidden channel (Like when it's used on the HiddenChannelLockScreen) @@ -325,6 +325,9 @@ export default definePlugin({ } ], + setEmojiComponent, + setChannelBeginHeaderComponent, + isHiddenChannel(channel: Channel & { channelId?: string; }) { if (!channel) return false; diff --git a/src/plugins/typingTweaks.tsx b/src/plugins/typingTweaks.tsx index c3f8f871d..3eb4fadec 100644 --- a/src/plugins/typingTweaks.tsx +++ b/src/plugins/typingTweaks.tsx @@ -70,7 +70,7 @@ export default definePlugin({ { find: "getCooldownTextStyle", replacement: { - match: /return \i\.Z\.getName\(.,.\.props\.channel\.id,(.)\)/, + match: /return \i\.\i\.getName\(.,.\.props\.channel\.id,(.)\)/, replace: "return $1" } }, diff --git a/src/plugins/webhookTags.ts b/src/plugins/webhookTags.ts index 51d68fa3b..96cbf3854 100644 --- a/src/plugins/webhookTags.ts +++ b/src/plugins/webhookTags.ts @@ -42,9 +42,9 @@ export default definePlugin({ { find: ".Types.ORIGINAL_POSTER", replacement: { - match: /return null==(.)\?null:\(0,.{1,3}\.jsxs?\)\((.{1,3})\.Z/, + match: /return null==(.)\?null:\(0,.{1,3}\.jsxs?\)\((.{1,3}\.\i)/, replace: (orig, type, BotTag) => - `if(arguments[0].message.webhookId&&arguments[0].user.isNonUserBot()){${type}=${BotTag}.Z.Types.WEBHOOK}${orig}`, + `if(arguments[0].message.webhookId&&arguments[0].user.isNonUserBot()){${type}=${BotTag}.Types.WEBHOOK}${orig}`, }, }, ], From 95db6c32a38b946abf90ebce165de82681159d68 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 5 Mar 2023 20:12:52 -0300 Subject: [PATCH 013/147] Fix Ignore Activities button on platforms different than Windows (#528) Co-authored-by: Ven --- src/plugins/ignoreActivities.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/ignoreActivities.tsx b/src/plugins/ignoreActivities.tsx index 56a0a8dba..3d53bbc0a 100644 --- a/src/plugins/ignoreActivities.tsx +++ b/src/plugins/ignoreActivities.tsx @@ -145,8 +145,11 @@ export default definePlugin({ patches: [{ find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", replacement: { - match: /var .=(?.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/, - replace: "$&,$self.renderToggleGameActivityButton($)" + match: /!(\i)\|\|(null==\i\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/, + replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "" + + `${restWithoutPlatformCheck}` + + `(${platformCheck}?${children}:[])` + + `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))` } }, { find: ".overlayBadge", From 36c27f1111d3a56536d21d2bbeade3f44da7eee0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 5 Mar 2023 22:39:53 -0300 Subject: [PATCH 014/147] VCDoubleClick: Fix applying to non voice channels (#572) --- src/plugins/vcDoubleClick.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/plugins/vcDoubleClick.ts b/src/plugins/vcDoubleClick.ts index 695e8c51f..6d124a6cd 100644 --- a/src/plugins/vcDoubleClick.ts +++ b/src/plugins/vcDoubleClick.ts @@ -19,7 +19,7 @@ import { migratePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { SelectedChannelStore } from "@webpack/common"; +import { ChannelStore, SelectedChannelStore } from "@webpack/common"; const timers = {} as Record(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)()", + match: /onClick:(\i)(?=,.{0,30}className:"channelMention".+?(\i)\.inContent)/, + replace: (_, onClick, props) => "" + + `onClick:(vcDoubleClickEvt)=>$self.shouldRunOnClick(vcDoubleClickEvt,${props})&&${onClick}()`, } } ], + shouldRunOnClick(e: MouseEvent, { channelId }) { + const channel = ChannelStore.getChannel(channelId); + if (!channel || ![2, 13].includes(channel.type)) return true; + return e.detail >= 2; + }, + schedule(cb: () => void, e: any) { const id = e.props.channel.id as string; if (SelectedChannelStore.getVoiceChannelId() === id) { From 7322c3af047489e122b59b01825ba5473dc96a62 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Mon, 6 Mar 2023 18:54:01 -0300 Subject: [PATCH 015/147] Fix Crash Loops and prevent metrics (#580) --- src/plugins/crashHandler.ts | 19 ++++++++++++++++++- src/plugins/noTrack.ts | 13 +++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/plugins/crashHandler.ts b/src/plugins/crashHandler.ts index 6f030b12f..79db7820c 100644 --- a/src/plugins/crashHandler.ts +++ b/src/plugins/crashHandler.ts @@ -41,6 +41,8 @@ const settings = definePluginSettings({ } }); +let crashCount: number = 0; + export default definePlugin({ name: "CrashHandler", description: "Utility plugin for handling and possibly recovering from Crashes without a restart", @@ -69,8 +71,22 @@ export default definePlugin({ ], handleCrash(_this: ReactElement & { forceUpdate: () => void; }) { + if (++crashCount > 5) { + try { + showNotification({ + color: "#eed202", + title: "Discord has crashed!", + body: "Awn :( Discord has crashed more than five times, not attempting to recover.", + }); + } catch { } + + return false; + } + + setTimeout(() => crashCount--, 60_000); + try { - maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true); + if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true); if (settings.store.attemptToPreventCrashes) { this.handlePreventCrash(_this); @@ -80,6 +96,7 @@ export default definePlugin({ return false; } catch (err) { CrashHandlerLogger.error("Failed to handle crash", err); + return false; } }, diff --git a/src/plugins/noTrack.ts b/src/plugins/noTrack.ts index 27ff430bd..617ab8b49 100644 --- a/src/plugins/noTrack.ts +++ b/src/plugins/noTrack.ts @@ -38,6 +38,19 @@ export default definePlugin({ match: /window\.DiscordSentry=function.+\}\(\)/, replace: "", } + }, + { + find: ".METRICS,", + replacement: [ + { + match: /this\._intervalId.+?12e4\)/, + replace: "" + }, + { + match: /(?<=increment=function\(\i\){)/, + replace: "return;" + } + ] } ] }); From 40395d562aeadaeaa88c5bc106797ea5a4ee51e4 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 8 Mar 2023 00:11:59 -0300 Subject: [PATCH 016/147] Improvements for patches and misc stuff (#582) --- src/plugins/apiNotices.ts | 7 +- src/plugins/crashHandler.ts | 4 +- src/plugins/disableDMCallIdle.ts | 10 +- src/plugins/experiments.tsx | 90 +++++++---- src/plugins/fakeNitro.ts | 22 +-- src/plugins/ignoreActivities.tsx | 65 ++++---- src/plugins/moyai.ts | 53 +++---- src/plugins/noScreensharePreview.ts | 8 +- src/plugins/pronoundb/index.ts | 10 +- src/plugins/revealAllSpoilers.ts | 4 +- src/plugins/showHiddenChannels/index.tsx | 191 ++++++++++++----------- src/plugins/spotifyCrack.ts | 78 ++++----- src/plugins/typingIndicator.tsx | 4 +- src/plugins/volumeBooster.ts | 56 +++---- src/plugins/whoReacted.tsx | 4 +- 15 files changed, 324 insertions(+), 282 deletions(-) diff --git a/src/plugins/apiNotices.ts b/src/plugins/apiNotices.ts index 8922aceed..af7cb15e8 100644 --- a/src/plugins/apiNotices.ts +++ b/src/plugins/apiNotices.ts @@ -29,13 +29,12 @@ export default definePlugin({ find: 'displayName="NoticeStore"', replacement: [ { - match: /;.{1,2}=null;.{0,70}getPremiumSubscription/g, - replace: - ";if(Vencord.Api.Notices.currentNotice)return false$&" + match: /(?=;\i=null;.{0,70}getPremiumSubscription)/g, + replace: ";if(Vencord.Api.Notices.currentNotice)return false" }, { match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/, - replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);' + replace: (_, notice) => `if(${notice}.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);` } ] } diff --git a/src/plugins/crashHandler.ts b/src/plugins/crashHandler.ts index 79db7820c..e35dfed6e 100644 --- a/src/plugins/crashHandler.ts +++ b/src/plugins/crashHandler.ts @@ -64,8 +64,8 @@ export default definePlugin({ { find: 'dispatch({type:"MODAL_POP_ALL"})', replacement: { - match: /(?<=(?\i)=function\(\){\(0,\i\.\i\)\(\);\i\.\i\.dispatch\({type:"MODAL_POP_ALL"}\).+};)/, - replace: "$self.popAllModals=$;" + match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/, + replace: (m, popAll) => `${m}$self.popAllModals=${popAll};` } } ], diff --git a/src/plugins/disableDMCallIdle.ts b/src/plugins/disableDMCallIdle.ts index c620f5447..2ba861cf2 100644 --- a/src/plugins/disableDMCallIdle.ts +++ b/src/plugins/disableDMCallIdle.ts @@ -27,9 +27,9 @@ export default definePlugin({ { find: ".Messages.BOT_CALL_IDLE_DISCONNECT", replacement: { - match: /function (?.{1,3})\(\){.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT.+?}}/, - replace: "function $(){}", - }, - }, - ], + match: /(?<=function \i\(\){)(?=.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/, + replace: "return;" + } + } + ] }); diff --git a/src/plugins/experiments.tsx b/src/plugins/experiments.tsx index a3125ae74..686b822d7 100644 --- a/src/plugins/experiments.tsx +++ b/src/plugins/experiments.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Settings } from "@api/settings"; +import { definePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; @@ -24,49 +24,71 @@ import { Forms, React } from "@webpack/common"; const KbdStyles = findByPropsLazy("key", "removeBuildOverride"); +const settings = definePluginSettings({ + enableIsStaff: { + description: "Enable isStaff", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + }, + forceStagingBanner: { + description: "Whether to force Staging banner under user area.", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + } +}); + export default definePlugin({ name: "Experiments", + description: "Enable Access to Experiments in Discord!", authors: [ Devs.Megu, Devs.Ven, Devs.Nickyux, - Devs.BanTheNons + Devs.BanTheNons, + Devs.Nuckyz ], - description: "Enable Access to Experiments in Discord!", - patches: [{ - find: "Object.defineProperties(this,{isDeveloper", - replacement: { - match: /(?<={isDeveloper:\{[^}]+,get:function\(\)\{return )\w/, - replace: "true" + settings, + + patches: [ + { + find: "Object.defineProperties(this,{isDeveloper", + replacement: { + match: /(?<={isDeveloper:\{[^}]+?,get:function\(\)\{return )\w/, + replace: "true" + } }, - }, { - find: 'type:"user",revision', - replacement: { - match: /!(\w{1,3})&&"CONNECTION_OPEN".+?;/g, - replace: "$1=!0;" + { + find: 'type:"user",revision', + replacement: { + match: /!(\i)&&"CONNECTION_OPEN".+?;/g, + replace: "$1=!0;" + } }, - }, { - find: ".isStaff=function(){", - predicate: () => Settings.plugins.Experiments.enableIsStaff === true, - replacement: [ - { - match: /return\s*(\w+)\.hasFlag\((.+?)\.STAFF\)}/, - replace: "return Vencord.Webpack.Common.UserStore.getCurrentUser().id===$1.id||$1.hasFlag($2.STAFF)}" - }, - { - match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*\|\|/, - replace: "hasFreePremium=function(){return ", - }, - ], - }], - options: { - enableIsStaff: { - description: "Enable isStaff (requires restart)", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: true, + { + find: ".isStaff=function(){", + predicate: () => settings.store.enableIsStaff, + replacement: [ + { + match: /return\s*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/, + replace: (_, user, flags) => `return Vencord.Webpack.Common.UserStore.getCurrentUser().id===${user}.id||${user}.hasFlag(${flags}.STAFF)}` + }, + { + match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*?\|\|/, + replace: "hasFreePremium=function(){return ", + } + ] + }, + { + find: ".Messages.DEV_NOTICE_STAGING", + predicate: () => settings.store.forceStagingBanner, + replacement: { + match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/, + replace: "true" + } } - }, + ], settingsAboutComponent: () => { const isMacOS = navigator.platform.includes("Mac"); diff --git a/src/plugins/fakeNitro.ts b/src/plugins/fakeNitro.ts index a09a95a39..e9b19ca1d 100644 --- a/src/plugins/fakeNitro.ts +++ b/src/plugins/fakeNitro.ts @@ -72,7 +72,7 @@ migratePluginSettings("FakeNitro", "NitroBypass"); export default definePlugin({ name: "FakeNitro", - authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain], + authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz], description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.", dependencies: ["MessageEventsAPI"], @@ -82,16 +82,16 @@ export default definePlugin({ predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true, replacement: [ { - match: /(?<=(?\i)=\i\.intention)/, - replace: ",fakeNitroIntention=$" + match: /(?<=(\i)=\i\.intention)/, + replace: (_, intention) => `,fakeNitroIntention=${intention}` }, { match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g, replace: ',typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0' }, { - match: /(?<=&&!\i&&)!(?\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, - replace: `(!$&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))` + match: /(?<=&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, + replace: (_, canUseExternal) => `(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))` } ] }, @@ -99,16 +99,16 @@ export default definePlugin({ find: "canUseAnimatedEmojis:function", predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true, replacement: { - match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?\i))\){(?.+?\))/g, - replace: `,fakeNitroIntention){$||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` + match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g, + replace: (_, premiumCheck) => `,fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` } }, { find: "canUseStickersEverywhere:function", predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true, replacement: { - match: /canUseStickersEverywhere:function\(.+?\{/, - replace: "$&return true;" + match: /(?<=canUseStickersEverywhere:function\(\i\){)/, + replace: "return true;" }, }, { @@ -128,8 +128,8 @@ export default definePlugin({ "canStreamMidQuality" ].map(func => { return { - match: new RegExp(`${func}:function\\(.+?\\{`), - replace: "$&return true;" + match: new RegExp(`(?<=${func}:function\\(\\i\\){)`), + replace: "return true;" }; }) }, diff --git a/src/plugins/ignoreActivities.tsx b/src/plugins/ignoreActivities.tsx index 3d53bbc0a..07b458f80 100644 --- a/src/plugins/ignoreActivities.tsx +++ b/src/plugins/ignoreActivities.tsx @@ -21,7 +21,7 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { useForceUpdater } from "@utils/misc"; import definePlugin from "@utils/types"; -import { findByPropsLazy } from "@webpack"; +import { findByPropsLazy, findStoreLazy } from "@webpack"; import { Tooltip } from "webpack/common"; enum ActivitiesTypes { @@ -37,7 +37,7 @@ interface IgnoredActivity { const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn"); const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon"); const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight"); -const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen"); +const RunningGameStore = findStoreLazy("RunningGameStore"); function ToggleIconOff() { return ( @@ -71,7 +71,7 @@ function ToggleIconOff() { ); } -function ToggleIconOn() { +function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) { return ( ); } -function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) { +function ToggleActivityComponent({ activity, forceWhite }: { activity: IgnoredActivity; forceWhite?: boolean; }) { const forceUpdate = useForceUpdater(); return ( @@ -105,7 +106,7 @@ function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) { { ignoredActivitiesCache.has(activity.id) ? - : + : }
)} @@ -117,9 +118,9 @@ function ToggleActivityComponentWithBackground({ activity }: { activity: Ignored return (
- +
); } @@ -142,28 +143,32 @@ export default definePlugin({ name: "IgnoreActivities", authors: [Devs.Nuckyz], description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.", - patches: [{ - find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", - replacement: { - match: /!(\i)\|\|(null==\i\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/, - replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "" - + `${restWithoutPlatformCheck}` - + `(${platformCheck}?${children}:[])` - + `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))` + patches: [ + { + find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", + replacement: { + match: /!(\i)\|\|(null==\i\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/, + replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "" + + `${restWithoutPlatformCheck}` + + `(${platformCheck}?${children}:[])` + + `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))` + } + }, + { + find: ".overlayBadge", + replacement: { + match: /(?<=\(\)\.badgeContainer.+?(\i)\.name}\):null)/, + replace: (_, props) => `,$self.renderToggleActivityButton(${props})` + } + }, + { + find: '.displayName="LocalActivityStore"', + replacement: { + match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/, + replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);` + } } - }, { - find: ".overlayBadge", - replacement: { - match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?.)\.name}\):null/, - replace: "$&,$self.renderToggleActivityButton($)" - } - }, { - find: '.displayName="LocalActivityStore"', - replacement: { - match: /(?.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?\)\)/, - replace: "$&;$=$.filter($self.isActivityNotIgnored);" - } - }], + ], async start() { const ignoredActivitiesData = await DataStore.get>("IgnoreActivities_ignoredActivities") ?? new Map(); @@ -217,5 +222,5 @@ export default definePlugin({ } } return true; - }, + } }); diff --git a/src/plugins/moyai.ts b/src/plugins/moyai.ts index fabc97fb4..146ac3f63 100644 --- a/src/plugins/moyai.ts +++ b/src/plugins/moyai.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Settings } from "@api/settings"; +import { definePluginSettings } from "@api/settings"; import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent"; import { Devs } from "@utils/constants"; import { sleep } from "@utils/misc"; @@ -54,15 +54,36 @@ const MOYAI = "🗿"; const MOYAI_URL = "https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3"; +const settings = definePluginSettings({ + volume: { + description: "Volume of the 🗿🗿🗿", + type: OptionType.SLIDER, + markers: makeRange(0, 1, 0.1), + default: 0.5, + stickToMarkers: false + }, + triggerWhenUnfocused: { + description: "Trigger the 🗿 even when the window is unfocused", + type: OptionType.BOOLEAN, + default: true + }, + ignoreBots: { + description: "Ignore bots", + type: OptionType.BOOLEAN, + default: true + } +}); + export default definePlugin({ name: "Moyai", authors: [Devs.Megu, Devs.Nuckyz], description: "🗿🗿🗿🗿🗿🗿🗿🗿", + settings, async onMessage(e: IMessageCreate) { if (e.optimistic || e.type !== "MESSAGE_CREATE") return; if (e.message.state === "SENDING") return; - if (Settings.plugins.Moyai.ignoreBots && e.message.author?.bot) return; + if (settings.store.ignoreBots && e.message.author?.bot) return; if (!e.message.content) return; if (e.channelId !== SelectedChannelStore.getChannelId()) return; @@ -76,7 +97,7 @@ export default definePlugin({ onReaction(e: IReactionAdd) { if (e.optimistic || e.type !== "MESSAGE_REACTION_ADD") return; - if (Settings.plugins.Moyai.ignoreBots && UserStore.getUser(e.userId)?.bot) return; + if (settings.store.ignoreBots && UserStore.getUser(e.userId)?.bot) return; if (e.channelId !== SelectedChannelStore.getChannelId()) return; const name = e.emoji.name.toLowerCase(); @@ -103,28 +124,6 @@ export default definePlugin({ FluxDispatcher.unsubscribe("MESSAGE_CREATE", this.onMessage); FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD", this.onReaction); FluxDispatcher.unsubscribe("VOICE_CHANNEL_EFFECT_SEND", this.onVoiceChannelEffect); - }, - - options: { - volume: { - description: "Volume of the 🗿🗿🗿", - type: OptionType.SLIDER, - markers: makeRange(0, 1, 0.1), - default: 0.5, - stickToMarkers: false, - }, - triggerWhenUnfocused: { - description: "Trigger the 🗿 even when the window is unfocused", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: false, - }, - ignoreBots: { - description: "Ignore bots", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: false, - } } }); @@ -158,9 +157,9 @@ function getMoyaiCount(message: string) { } function boom() { - if (!Settings.plugins.Moyai.triggerWhenUnfocused && !document.hasFocus()) return; + if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return; const audioElement = document.createElement("audio"); audioElement.src = MOYAI_URL; - audioElement.volume = Settings.plugins.Moyai.volume; + audioElement.volume = settings.store.volume; audioElement.play(); } diff --git a/src/plugins/noScreensharePreview.ts b/src/plugins/noScreensharePreview.ts index b09f20636..50b2a964f 100644 --- a/src/plugins/noScreensharePreview.ts +++ b/src/plugins/noScreensharePreview.ts @@ -30,9 +30,9 @@ export default definePlugin({ "\\i\\.default\\.makeChunkedRequest\\(", "\\i\\.\\i\\.post\\({url:" ].map(match => ({ - match: new RegExp(`return\\[(?\\d),${match}\\i\\.\\i\\.STREAM_PREVIEW.+?}\\)\\];`), - replace: 'return[$,Promise.resolve({body:"",status:204})];' + match: new RegExp(`(?=return\\[(\\d),${match}\\i\\.\\i\\.STREAM_PREVIEW.+?}\\)\\];)`), + replace: (_, code) => `return[${code},Promise.resolve({body:"",status:204})];` })) - }, - ], + } + ] }); diff --git a/src/plugins/pronoundb/index.ts b/src/plugins/pronoundb/index.ts index 7ebe91921..820b66eca 100644 --- a/src/plugins/pronoundb/index.ts +++ b/src/plugins/pronoundb/index.ts @@ -41,7 +41,7 @@ export default definePlugin({ replace: "[$1, $self.PronounsChatComponent(e)]" } }, - // Hijack the discord pronouns section (hidden without experiment) and add a wrapper around the text section + // Hijack the discord pronouns section and add a wrapper around the text section { find: ".Messages.BOT_PROFILE_SLASH_COMMANDS", replacement: { @@ -49,12 +49,12 @@ export default definePlugin({ replace: "$&&$self.PronounsProfileWrapper($,$,$)" } }, - // Make pronouns experiment be enabled by default + // Force enable pronouns component ignoring the experiment value { - find: "2022-01_pronouns", + find: ".Messages.USER_POPOUT_PRONOUNS", replacement: { - match: "!1", // false - replace: "!0" + match: /\i\.\i\.useExperiment\({}\)\.showPronouns/, + replace: "true" } } ], diff --git a/src/plugins/revealAllSpoilers.ts b/src/plugins/revealAllSpoilers.ts index ead169fce..9cb7b6b45 100644 --- a/src/plugins/revealAllSpoilers.ts +++ b/src/plugins/revealAllSpoilers.ts @@ -32,8 +32,8 @@ export default definePlugin({ { find: ".removeObscurity=function", replacement: { - match: /\.removeObscurity=function\((\i)\){/, - replace: ".removeObscurity=function($1){$self.reveal($1);" + match: /(?<=\.removeObscurity=function\((\i)\){)/, + replace: (_, event) => `$self.reveal(${event});` } } ], diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 60e0c51fa..70c5045ba 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -64,29 +64,29 @@ export default definePlugin({ patches: [ { // RenderLevel defines if a channel is hidden, collapsed in category, visible, etc - find: ".CannotShow", + find: ".CannotShow=", // These replacements only change the necessary CannotShow's replacement: [ { - match: /(?<=isChannelGatedAndVisible\(this\.record\.guild_id,this\.record\.id\).+?renderLevel:)(?\i)\..+?(?=,)/, - replace: "this.category.isCollapsed?$.WouldShowIfUncollapsed:$.Show" + match: /(?<=isChannelGatedAndVisible\(this\.record\.guild_id,this\.record\.id\).+?renderLevel:)(\i)\..+?(?=,)/, + replace: (_, RenderLevels) => `this.category.isCollapsed?${RenderLevels}.WouldShowIfUncollapsed:${RenderLevels}.Show` }, // Move isChannelGatedAndVisible renderLevel logic to the bottom to not show hidden channels in case they are muted { - match: /(?<=(?if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{)if\(this\.id===\i\).+?};)(?if\(!\i\.\i\.isChannelGatedAndVisible\(.+?})(?.+?)(?=return{renderLevel:\i\.Show.{1,40}return \i)/, - replace: "$$$}" + match: /(?<=(if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{)if\(this\.id===\i\).+?};)(if\(!\i\.\i\.isChannelGatedAndVisible\(.+?})(.+?)(?=return{renderLevel:\i\.Show.{0,40}?return \i)/, + replace: (_, permissionCheck, isChannelGatedAndVisibleCondition, rest) => `${rest}${permissionCheck}${isChannelGatedAndVisibleCondition}}` }, { - match: /(?<=renderLevel:(?\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/, - replace: "$" + match: /(?<=renderLevel:(\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/, + replace: (_, renderLevelExpression) => renderLevelExpression }, { - match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(?\i)\..+?(?=,)/, - replace: "$.Show" + match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(\i)\..+?(?=,)/, + replace: (_, RenderLevels) => `${RenderLevels}.Show` }, { - match: /(?<=getRenderLevel=function.+?return ).+?\?(?.+?):\i\.CannotShow(?=})/, - replace: "$" + match: /(?<=getRenderLevel=function.+?return ).+?\?(.+?):\i\.CannotShow(?=})/, + replace: (_, renderLevelExpressionWithoutPermCheck) => renderLevelExpressionWithoutPermCheck } ] }, @@ -95,18 +95,18 @@ export default definePlugin({ replacement: [ { // Do not show confirmation to join a voice channel when already connected to another if clicking on a hidden voice channel - match: /(?<=getCurrentClientVoiceChannelId\(\i\.guild_id\);if\()(?=.+?\((?\i)\))/, - replace: "!$self.isHiddenChannel($)&&" + match: /(?<=getCurrentClientVoiceChannelId\((\i)\.guild_id\);if\()/, + replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&` }, { // Make Discord think we are connected to a voice channel so it shows us inside it - match: /(?=\|\|\i\.default\.selectVoiceChannel\((?\i)\.id\))/, - replace: "||$self.isHiddenChannel($)" + match: /(?=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\))/, + replace: (_, channel) => `||$self.isHiddenChannel(${channel})` }, { // Make Discord think we are connected to a voice channel so it shows us inside it - match: /(?<=\|\|\i\.default\.selectVoiceChannel\((?\i)\.id\);!__OVERLAY__&&\()/, - replace: "$self.isHiddenChannel($)||" + match: /(?<=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\);!__OVERLAY__&&\()/, + replace: (_, channel) => `$self.isHiddenChannel(${channel})||` } ] }, @@ -119,7 +119,7 @@ export default definePlugin({ "renderInviteButton", "renderOpenChatButton" ].map(func => ({ - match: new RegExp(`(?<=\\i\\.${func}=function\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions + match: new RegExp(`(?<=${func}=function\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions replace: "if($self.isHiddenChannel(this.props.channel))return null;" })) ] @@ -129,17 +129,8 @@ export default definePlugin({ predicate: () => settings.store.showMode === ShowMode.LockIcon, replacement: { // Lock Icon - match: /(?=switch\((?\i)\.type\).{1,30}\.GUILD_ANNOUNCEMENT.{1,30}\(0,\i\.\i\))/, - replace: "if($self.isHiddenChannel($))return $self.LockIcon;" - } - }, - { - find: ".UNREAD_HIGHLIGHT", - predicate: () => settings.store.hideUnreads === true, - replacement: { - // Hide unreads - match: /(?<=\i\.connected,\i=)(?=(?\i)\.unread)/, - replace: "$self.isHiddenChannel($.channel)?false:" + match: /(?=switch\((\i)\.type\).{0,30}\.GUILD_ANNOUNCEMENT.{0,30}\(0,\i\.\i\))/, + replace: (_, channel) => `if($self.isHiddenChannel(${channel}))return $self.LockIcon;` } }, { @@ -148,36 +139,44 @@ export default definePlugin({ replacement: [ // Make the channel appear as muted if it's hidden { - match: /(?<=\i\.name,\i=)(?=(?\i)\.muted)/, - replace: "$self.isHiddenChannel($.channel)?true:" + match: /(?<=\i\.name,\i=)(?=(\i)\.muted)/, + replace: (_, props) => `$self.isHiddenChannel(${props}.channel)?true:` }, // Add the hidden eye icon if the channel is hidden { - match: /(?<=(?\i)=\i\.channel,.+?\(\)\.children.+?:null)/, - replace: ",$self.isHiddenChannel($)?$self.HiddenChannelIcon():null" + match: /\(\).children.+?:null(?<=(\i)=\i\.channel,.+?)/, + replace: (m, channel) => `${m},$self.isHiddenChannel(${channel})?$self.HiddenChannelIcon():null` }, // Make voice channels also appear as muted if they are muted { - match: /(?<=\i\(\)\.wrapper:\i\(\)\.notInteractive,)(?.+?)(?(?\i)\?\i\.MUTED)/, - replace: "$:\"\",$$?\"\"" + match: /(?<=\.wrapper:\i\(\)\.notInteractive,)(.+?)((\i)\?\i\.MUTED)/, + replace: (_, otherClasses, mutedClassExpression, isMuted) => `${mutedClassExpression}:"",${otherClasses}${isMuted}?""` } ] }, - // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden { find: ".UNREAD_HIGHLIGHT", - predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, - replacement: { - match: /(?<=(?\i)=\i\.channel,.+?\.LOCKED:\i)/, - replace: "&&!($self.settings.store.hideUnreads===false&&$self.isHiddenChannel($))" - } + replacement: [ + { + // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden + predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, + match: /\.LOCKED:\i(?<=(\i)=\i\.channel,.+?)/, + replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})` + }, + { + // Hide unreads + predicate: () => settings.store.hideUnreads === true, + match: /(?<=\i\.connected,\i=)(?=(\i)\.unread)/, + replace: (_, props) => `$self.isHiddenChannel(${props}.channel)?false:` + } + ] }, { // Hide New unreads box for hidden channels find: '.displayName="ChannelListUnreadsStore"', replacement: { - match: /(?<=return null!=(?\i))(?=.{1,130}hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module - replace: "&&!$self.isHiddenChannel($)" + match: /(?<=return null!=(\i))(?=.{0,130}?hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module + replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` } }, // Only render the channel header and buttons that work when transitioning to a hidden channel @@ -185,20 +184,20 @@ export default definePlugin({ find: "Missing channel in Channel.renderHeaderToolbar", replacement: [ { - match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_TEXT:)(?=.+?;(?.+?{channel:(?\i)},"notifications"\)\);))/, - replace: "if($self.isHiddenChannel($)){$break;}" + match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_TEXT:)(?=.+?;(.+?{channel:(\i)},"notifications"\)\);))/, + replace: (_, pushNotificationButtonExpression, channel) => `if($self.isHiddenChannel(${channel})){${pushNotificationButtonExpression}break;}` }, { - match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(?.+?{channel:(?\i)},"notifications"\)\)))/, - replace: "if($self.isHiddenChannel($)){$;break;}" + match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(.+?{channel:(\i)},"notifications"\)\)))/, + replace: (_, pushNotificationButtonExpression, channel) => `if($self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` }, { - match: /(?<=(?\i)\.renderMobileToolbar=function.+?case \i\.\i\.GUILD_FORUM:)/, - replace: "if($self.isHiddenChannel($.props.channel))break;" + match: /renderMobileToolbar=function.+?case \i\.\i\.GUILD_FORUM:(?<=(\i)\.renderMobileToolbar.+?)/, + replace: (m, that) => `${m}if($self.isHiddenChannel(${that}.props.channel))break;` }, { - match: /(?<=renderHeaderBar=function.+?hideSearch:(?\i)\.isDirectory\(\))/, - replace: "||$self.isHiddenChannel($)" + match: /(?<=renderHeaderBar=function.+?hideSearch:(\i)\.isDirectory\(\))/, + replace: (_, channel) => `||$self.isHiddenChannel(${channel})` }, { match: /(?<=renderSidebar=function\(\){)/, @@ -213,25 +212,23 @@ export default definePlugin({ // Avoid trying to fetch messages from hidden channels { find: '"MessageManager"', - replacement: [ - { - match: /(?<=if\(null!=(?\i)\).{1,100}"Skipping fetch because channelId is a static route".{1,10}else{)/, - replace: "if($self.isHiddenChannel({channelId:$}))return;" - }, - ] + replacement: { + match: /"Skipping fetch because channelId is a static route"\);else{(?=.+?getChannel\((\i)\))/, + replace: (m, channelId) => `${m}if($self.isHiddenChannel({channelId:${channelId}}))return;` + } }, // Patch keybind handlers so you can't accidentally jump to hidden channels { find: '"alt+shift+down"', replacement: { - match: /(?<=getChannel\(\i\);return null!=(?\i))(?=.{1,130}hasRelevantUnread\(\i\))/, - replace: "&&!$self.isHiddenChannel($)" + match: /(?<=getChannel\(\i\);return null!=(\i))(?=.{0,130}?hasRelevantUnread\(\i\))/, + replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` } }, { find: '"alt+down"', replacement: { - match: /(?<=getState\(\)\.channelId.{1,30}\(0,\i\.\i\)\(\i\))(?=\.map\()/, + match: /(?<=getState\(\)\.channelId.{0,30}?\(0,\i\.\i\)\(\i\))(?=\.map\()/, replace: ".filter(ch=>!$self.isHiddenChannel(ch))" } }, @@ -239,8 +236,8 @@ export default definePlugin({ { find: 'jumboable?"jumbo":"default"', replacement: { - match: /(?<=(?\i)=function.{1,20}node,\i=\i.isInteracting.+?}}\)},)/, - replace: "shcEmojiComponentExport=($self.setEmojiComponent($),void 0)," + match: /jumboable\?"jumbo":"default",emojiId.+?}}\)},(?<=(\i)=function\(\i\){var \i=\i\.node.+?)/, + replace: (m, component) => `${m}shcEmojiComponentExport=($self.setEmojiComponent(${component}),void 0),` } }, { @@ -248,13 +245,13 @@ export default definePlugin({ replacement: [ { // Export the channel beggining header - match: /(?<=function (?\i)\(.{1,600}computePermissionsForRoles.+?}\)})(?=var)/, - replace: "$self.setChannelBeginHeaderComponent($);" + match: /computePermissionsForRoles.+?}\)}(?<=function (\i)\(.+?)(?=var)/, + replace: (m, component) => `${m}$self.setChannelBeginHeaderComponent(${component});` }, { // Patch the header to only return allowed users and roles if it's a hidden channel (Like when it's used on the HiddenChannelLockScreen) - match: /(?<=MANAGE_ROLES.{1,60}return)(?=\(.+?(?\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(?\i)\.guild_id.+?roleColor.+?]}\)))/, - replace: " $self.isHiddenChannel($)?$:" + match: /MANAGE_ROLES.{0,60}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?]}\)))/, + replace: (m, component, channel) => `${m} $self.isHiddenChannel(${channel})?${component}:` } ] }, @@ -263,23 +260,23 @@ export default definePlugin({ replacement: [ { // Remove the divider and the open chat button for the HiddenChannelLockScreen - match: /(?<=function \i\((?\i)\).{1,2000}"more-options-popout"\)\);if\()/, - replace: "(!$self.isHiddenChannel($.channel)||$.inCall)&&" + match: /"more-options-popout"\)\);if\((?<=function \i\((\i)\).+?)/, + replace: (m, props) => `${m}(!$self.isHiddenChannel(${props}.channel)||${props}.inCall)&&` }, { // Render our HiddenChannelLockScreen component instead of the main voice channel component - match: /(?<=renderContent=function.{1,1700}children:)/, - replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?$self.HiddenChannelLockScreen(this.props.channel):" + match: /this\.renderVoiceChannelEffects.+?children:(?<=renderContent=function.+?)/, + replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?$self.HiddenChannelLockScreen(this.props.channel):" }, { // Disable gradients for the HiddenChannelLockScreen of voice channels - match: /(?<=renderContent=function.{1,1600}disableGradients:)/, - replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)||" + match: /this\.renderVoiceChannelEffects.+?disableGradients:(?<=renderContent=function.+?)/, + replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)||" }, { // Disable useless components for the HiddenChannelLockScreen of voice channels - match: /(?<=renderContent=function.{1,800}render(?!Header).{0,30}:)(?!void)/g, - replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?null:" + match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:(?<=renderContent=function.+?)(?!void)/g, + replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?null:" } ] }, @@ -288,40 +285,58 @@ export default definePlugin({ replacement: [ { // Render our HiddenChannelLockScreen component instead of the main stage channel component - match: /(?<=(?\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1400}children:)(?=.{1,20}}\)}function)/, - replace: "$self.isHiddenChannel($)?$self.HiddenChannelLockScreen($):" + match: /Guild voice channel without guild id.+?children:(?<=(\i)\.getGuildId\(\).+?)(?=.{0,20}?}\)}function)/, + replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?$self.HiddenChannelLockScreen(${channel}):` }, { // Disable useless components for the HiddenChannelLockScreen of stage channels - match: /(?<=(?\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1000}render(?!Header).{0,30}:)/g, - replace: "$self.isHiddenChannel($)?null:" + match: /render(?!Header).{0,30}?:(?<=(\i)\.getGuildId\(\).+?Guild voice channel without guild id.+?)/g, + replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?null:` }, // Prevent Discord from replacing our route if we aren't connected to the stage channel { - match: /(?<=if\()(?=!\i&&!\i&&!\i.{1,80}(?\i)\.getGuildId\(\).{1,50}Guild voice channel without guild id\.)/, - replace: "!$self.isHiddenChannel($)&&" + match: /(?=!\i&&!\i&&!\i.{0,80}?(\i)\.getGuildId\(\).{0,50}?Guild voice channel without guild id)(?<=if\()/, + replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&` }, { // Disable gradients for the HiddenChannelLockScreen of stage channels - match: /(?<=(?\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}disableGradients:)/, - replace: "$self.isHiddenChannel($)||" + match: /Guild voice channel without guild id.+?disableGradients:(?<=(\i)\.getGuildId\(\).+?)/, + replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})||` }, { // Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels - match: /(?<=(?\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}style:)/, - replace: "$self.isHiddenChannel($)?undefined:" + match: /Guild voice channel without guild id.+?style:(?<=(\i)\.getGuildId\(\).+?)/, + replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?undefined:` }, { // Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen - match: /\(0,\i\.jsx\)\(\i\.\i\.Divider.+?}\)]}\)(?=.+?:(?\i)\.guild_id)/, - replace: "$self.isHiddenChannel($)?null:($&)" + match: /\(0,\i\.jsx\)\(\i\.\i\.Divider.+?}\)]}\)(?=.+?:(\i)\.guild_id)/, + replace: (m, channel) => `$self.isHiddenChannel(${channel})?null:(${m})` }, { // Remove the open chat button for the HiddenChannelLockScreen - match: /(?<=null,)(?=.{1,120}channelId:(?\i)\.id,.+?toggleRequestToSpeakSidebar:\i,iconClassName:\i\(\)\.buttonIcon)/, - replace: "!$self.isHiddenChannel($)&&" + match: /"recents".+?null,(?=.{0,120}?channelId:(\i)\.id)/, + replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&` } ], + }, + { + // The module wasn't being found, so lets just escape everything + // eslint-disable-next-line no-useless-escape + find: "\^https\:\/\/\(\?\:canary\.\|ptb\.\)\?discord.com\/channels\/\(\\\\\d\+\|", + replacement: { + // Make mentions of hidden channels work + match: /\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,\i\)/, + replace: "true" + }, + }, + { + find: ".shouldCloseDefaultModals", + replacement: { + // Show inside voice channel instead of trying to join them when clicking on a channel mention + match: /(?<=getChannel\((\i)\)\)(?=.{0,100}?selectVoiceChannel))/, + replace: (_, channelId) => `&&!$self.isHiddenChannel({channelId:${channelId}})` + } } ], diff --git a/src/plugins/spotifyCrack.ts b/src/plugins/spotifyCrack.ts index c64154aaf..2682ccb46 100644 --- a/src/plugins/spotifyCrack.ts +++ b/src/plugins/spotifyCrack.ts @@ -16,53 +16,55 @@ * along with this program. If not, see . */ -import { migratePluginSettings, Settings } from "@api/settings"; +import { definePluginSettings, migratePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; +const settings = definePluginSettings({ + noSpotifyAutoPause: { + description: "Disable Spotify auto-pause", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true + }, + keepSpotifyActivityOnIdle: { + description: "Keep Spotify activity playing when idling", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + } +}); + migratePluginSettings("SpotifyCrack", "Ify"); export default definePlugin({ name: "SpotifyCrack", description: "Free listen along, no auto-pausing in voice chat, and allows activity to continue playing when idling", - authors: [ - Devs.Cyn, - Devs.Nuckyz - ], + authors: [Devs.Cyn, Devs.Nuckyz], + settings, - patches: [{ - find: 'dispatch({type:"SPOTIFY_PROFILE_UPDATE"', - replacement: [{ - match: /(function\((.{1,2})\){)(.{1,6}dispatch\({type:"SPOTIFY_PROFILE_UPDATE")/, - replace: (_, functionStart, data, functionBody) => `${functionStart}${data}.body.product="premium";${functionBody}` - }], - }, { - find: '.displayName="SpotifyStore"', - predicate: () => Settings.plugins.SpotifyCrack.noSpotifyAutoPause, - replacement: { - match: /function (.{1,2})\(\).{0,200}SPOTIFY_AUTO_PAUSED\);.{0,}}}}/, - replace: "function $1(){}" - } - }, { - find: '.displayName="SpotifyStore"', - predicate: () => Settings.plugins.SpotifyCrack.keepSpotifyActivityOnIdle, - replacement: { - match: /(shouldShowActivity=function\(\){.{1,50})&&!.{1,6}\.isIdle\(\)(.{0,}?})/, - replace: (_, functionDeclarationAndExpression, restOfFunction) => `${functionDeclarationAndExpression}${restOfFunction}` - } - }], + patches: [ + { - options: { - noSpotifyAutoPause: { - description: "Disable Spotify auto-pause", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true, + find: 'dispatch({type:"SPOTIFY_PROFILE_UPDATE"', + replacement: { + match: /SPOTIFY_PROFILE_UPDATE.+?isPremium:(?="premium"===(\i)\.body\.product)/, + replace: (m, req) => `${m}(${req}.body.product="premium")&&` + }, }, - keepSpotifyActivityOnIdle: { - description: "Keep Spotify activity playing when idling", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: true, + { + find: '.displayName="SpotifyStore"', + replacement: [ + { + predicate: () => settings.store.noSpotifyAutoPause, + match: /(?<=function \i\(\){)(?=.{0,200}SPOTIFY_AUTO_PAUSED\))/, + replace: "return;" + }, + { + predicate: () => settings.store.keepSpotifyActivityOnIdle, + match: /(?<=shouldShowActivity=function\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/, + replace: "" + } + ] } - } + ] }); diff --git a/src/plugins/typingIndicator.tsx b/src/plugins/typingIndicator.tsx index 27c143b02..9af09bca6 100644 --- a/src/plugins/typingIndicator.tsx +++ b/src/plugins/typingIndicator.tsx @@ -121,8 +121,8 @@ export default definePlugin({ { find: ".UNREAD_HIGHLIGHT", replacement: { - match: /(?<=(?\i)=\i\.channel,.+?\(\)\.children.+?:null)/, - replace: ",$self.TypingIndicator($.id)" + match: /\(\).children.+?:null(?<=(\i)=\i\.channel,.+?)/, + replace: (m, channel) => `${m},$self.TypingIndicator(${channel}.id)` } } ], diff --git a/src/plugins/volumeBooster.ts b/src/plugins/volumeBooster.ts index 6553a5c78..7d8144912 100644 --- a/src/plugins/volumeBooster.ts +++ b/src/plugins/volumeBooster.ts @@ -16,14 +16,26 @@ * along with this program. If not, see . */ -import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent"; +import { definePluginSettings } from "@api/settings"; +import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; +const settings = definePluginSettings({ + multiplier: { + description: "Volume Multiplier", + type: OptionType.SLIDER, + markers: makeRange(1, 5, 1), + default: 2, + stickToMarkers: true, + } +}); + export default definePlugin({ name: "VolumeBooster", authors: [Devs.Nuckyz], description: "Allows you to set the user and stream volume above the default maximum.", + settings, patches: [ // Change the max volume for sliders to allow for values above 200 @@ -33,11 +45,10 @@ export default definePlugin({ ].map(find => ({ find, replacement: { - match: /maxValue:(?\i\.\i)\?(?\d+?):(?\d+?),/, - replace: "" - + "maxValue:$" - + "?$*Vencord.Settings.plugins.VolumeBooster.multiplier" - + ":$*Vencord.Settings.plugins.VolumeBooster.multiplier," + match: /(?<=maxValue:\i\.\i)\?(\d+?):(\d+?)(?=,)/, + replace: (_, higherMaxVolume, minorMaxVolume) => "" + + `?${higherMaxVolume}*$self.settings.store.multiplier` + + `:${minorMaxVolume}*$self.settings.store.multiplier` } })), // Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord @@ -45,16 +56,16 @@ export default definePlugin({ find: "AudioContextSettingsMigrated", replacement: [ { - match: /(?updateAsync\("audioContextSettings".{1,50})(?return (?\i)\.volume=(?\i))/, - replace: "$if($>200)return $.volume=200;$" + match: /(?<=updateAsync\("audioContextSettings".{0,50})(?=return (\i)\.volume=(\i))/, + replace: (_, volumeOptions, newVolume) => `if(${newVolume}>200)return ${volumeOptions}.volume=200;` }, { - match: /(?Object\.entries\(\i\.localMutes\).+?)volume:(?.+?),/, - replace: "$volume:$>200?200:$," + match: /(?<=Object\.entries\(\i\.localMutes\).+?volume:).+?(?=,)/, + replace: "$&>200?200:$&" }, { - match: /(?Object\.entries\(\i\.localVolumes\).+?)volume:(?.+?)}\)/, - replace: "$volume:$>200?200:$})" + match: /(?<=Object\.entries\(\i\.localVolumes\).+?volume:).+?(?=})/, + replace: "$&>200?200:$&" } ] }, @@ -63,24 +74,13 @@ export default definePlugin({ find: '.displayName="MediaEngineStore"', replacement: [ { - match: /(?\.settings\.audioContextSettings.+?)(?\i\[\i\])=(?\i\.volume)(?.+?)setLocalVolume\((?.+?),.+?\)/, - replace: "" - + "$" - + "($>200?undefined:$=$)" - + "$" - + "setLocalVolume($,$??$)" + match: /(?<=\.settings\.audioContextSettings.+?)(\i\[\i\])=(\i\.volume)(.+?setLocalVolume\(\i,).+?\)/, + replace: (_, localVolume, syncVolume, rest) => "" + + `(${localVolume}>200?void 0:${localVolume}=${syncVolume})` + + rest + + `${localVolume}??${syncVolume})` } ] } ], - - options: { - multiplier: { - description: "Volume Multiplier", - type: OptionType.SLIDER, - markers: makeRange(1, 5, 1), - default: 2, - stickToMarkers: true, - } - } }); diff --git a/src/plugins/whoReacted.tsx b/src/plugins/whoReacted.tsx index 8ab1c5f7d..4b1c828fd 100644 --- a/src/plugins/whoReacted.tsx +++ b/src/plugins/whoReacted.tsx @@ -93,8 +93,8 @@ export default definePlugin({ patches: [{ find: ",reactionRef:", replacement: { - match: /((.)=(.{1,3})\.hideCount)(,.+?reactionCount.+?\}\))/, - replace: "$1,whoReactedProps=$3$4,$2?null:$self.renderUsers(whoReactedProps)" + match: /(?<=(\i)=(\i)\.hideCount,)(.+?reactionCount.+?\}\))/, + replace: (_, hideCount, props, rest) => `whoReactedProps=${props},${rest},${hideCount}?null:$self.renderUsers(whoReactedProps)` } }], From 1b199ec5d8e0ca3805a9960323ddd267561b4cf6 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 8 Mar 2023 01:59:50 -0300 Subject: [PATCH 017/147] feat: Context Menu API (#496) --- src/api/ContextMenu.ts | 141 ++++++++++++++++++++++++++++++++++ src/api/index.ts | 6 ++ src/plugins/apiContextMenu.ts | 69 +++++++++++++++++ src/webpack/patchWebpack.ts | 10 ++- src/webpack/webpack.ts | 23 +++--- 5 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 src/api/ContextMenu.ts create mode 100644 src/plugins/apiContextMenu.ts diff --git a/src/api/ContextMenu.ts b/src/api/ContextMenu.ts new file mode 100644 index 000000000..64671177b --- /dev/null +++ b/src/api/ContextMenu.ts @@ -0,0 +1,141 @@ +/* + * 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 Logger from "@utils/Logger"; +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 + */ +export type NavContextMenuPatchCallback = (children: Array, args?: Array) => void; +/** + * @param 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 + */ +export type GlobalContextMenuPatchCallback = (navId: string, children: Array, args?: Array) => void; + +const ContextMenuLogger = new Logger("ContextMenu"); + +export const navPatches = new Map>(); +export const globalPatches = new Set(); + +/** + * 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, 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 + * @returns Wheter the patch was sucessfully removed from the context menu(s) + */ +export function removeContextMenuPatch>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array { + 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; +} + +/** + * Remove a global context menu patch + * @returns Wheter the patch was sucessfully removed + */ +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 of one of its childs + * @param id The id of the child + */ +export function findGroupChildrenByChildId(id: string, children: Array, itemsArray?: Array): Array | null { + for (const child of children) { + if (child === null) continue; + + if (child.props?.id === id) return itemsArray ?? null; + + let nextChildren = child.props?.children; + if (nextChildren) { + if (!Array.isArray(nextChildren)) { + nextChildren = [nextChildren]; + child.props.children = nextChildren; + } + + const found = findGroupChildrenByChildId(id, nextChildren, nextChildren); + if (found !== null) return found; + } + } + + return null; +} + +interface ContextMenuProps { + contextMenuApiArguments?: Array; + navId: string; + children: Array; + "aria-label": string; + onSelect: (() => void) | undefined; + onClose: (callback: (...args: Array) => any) => void; +} + +export function _patchContextMenu(props: ContextMenuProps) { + const contextMenuPatches = navPatches.get(props.navId); + + if (contextMenuPatches) { + for (const patch of contextMenuPatches) { + try { + patch(props.children, props.contextMenuApiArguments); + } catch (err) { + ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); + } + } + } + + for (const patch of globalPatches) { + try { + patch(props.navId, props.children, props.contextMenuApiArguments); + } catch (err) { + ContextMenuLogger.error("Global patch errored,", err); + } + } +} diff --git a/src/api/index.ts b/src/api/index.ts index abb509348..e4b87bfce 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -18,6 +18,7 @@ import * as $Badges from "./Badges"; import * as $Commands from "./Commands"; +import * as $ContextMenu from "./ContextMenu"; import * as $DataStore from "./DataStore"; import * as $MemberListDecorators from "./MemberListDecorators"; import * as $MessageAccessories from "./MessageAccessories"; @@ -93,3 +94,8 @@ export const Styles = $Styles; * An API allowing you to display notifications */ export const Notifications = $Notifications; + +/** + * An api allowing you to patch and add/remove items to/from context menus + */ +export const ContextMenu = $ContextMenu; diff --git a/src/plugins/apiContextMenu.ts b/src/plugins/apiContextMenu.ts new file mode 100644 index 000000000..131c209ed --- /dev/null +++ b/src/plugins/apiContextMenu.ts @@ -0,0 +1,69 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 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 { Settings } from "@api/settings"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { addListener, removeListener } from "@webpack"; + +function listener(exports: any, id: number) { + if (typeof exports !== "object" || exports === null) return; + + for (const key in exports) if (key.length <= 3) { + const prop = exports[key]; + if (typeof prop !== "function") continue; + + const str = Function.prototype.toString.call(prop); + if (str.includes('path:["empty"]')) { + Vencord.Plugins.patches.push({ + plugin: "ContextMenuAPI", + all: true, + noWarn: true, + find: "navId:", + replacement: { + /** Regex explanation + * Use of https://blog.stevenlevithan.com/archives/mimic-atomic-groups to mimick atomic groups: (?=(...))\1 + * Match ${id} and look behind it for the first match of `=`: ${id}(?=(\i)=.+?) + * Match rest of the code until it finds `.${key},{`: .+?\2\.${key},{ + */ + match: RegExp(`(?=(${id}(?<=(\\i)=.+?).+?\\2\\.${key},{))\\1`, "g"), + replace: "$&contextMenuApiArguments:arguments," + } + }); + + removeListener(listener); + } + } +} + +if (Settings.plugins.ContextMenuAPI.enabled) addListener(listener); + +export default definePlugin({ + name: "ContextMenuAPI", + description: "API for adding/removing items to/from context menus.", + authors: [Devs.Nuckyz], + patches: [ + { + find: "♫ (つ。◕‿‿◕。)つ ♪", + replacement: { + match: /(?<=function \i\((\i)\){)(?=var \i,\i=\i\.navId)/, + replace: (_, props) => `Vencord.Api.ContextMenu._patchContextMenu(${props});` + } + } + ] +}); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 19ca9517b..697ce9496 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -92,9 +92,11 @@ function patchPush() { return; } + const numberId = Number(id); + for (const callback of listeners) { try { - callback(exports); + callback(exports, numberId); } catch (err) { logger.error("Error in webpack listener", err); } @@ -104,17 +106,17 @@ function patchPush() { try { if (filter(exports)) { subscriptions.delete(filter); - callback(exports); + callback(exports, numberId); } else if (typeof exports === "object") { if (exports.default && filter(exports.default)) { subscriptions.delete(filter); - callback(exports.default); + callback(exports.default, numberId); } for (const nested in exports) if (nested.length <= 3) { if (exports[nested] && filter(exports[nested])) { subscriptions.delete(filter); - callback(exports[nested]); + callback(exports[nested], numberId); } } } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 98a0ea89f..0d9558790 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -57,7 +57,7 @@ export const filters = { export const subscriptions = new Map(); export const listeners = new Set(); -export type CallbackFn = (mod: any) => void; +export type CallbackFn = (mod: any, id: number) => void; export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { if (cache !== void 0) throw "no."; @@ -86,18 +86,23 @@ export const find = traceFunction("find", function find(filter: FilterFn, getDef const mod = cache[key]; if (!mod?.exports) continue; - if (filter(mod.exports)) - return mod.exports; + if (filter(mod.exports)) { + return isWaitFor ? [mod.exports, Number(key)] : mod.exports; + } if (typeof mod.exports !== "object") continue; - if (mod.exports.default && filter(mod.exports.default)) - return getDefault ? mod.exports.default : mod.exports; + if (mod.exports.default && filter(mod.exports.default)) { + const found = getDefault ? mod.exports.default : mod.exports; + return isWaitFor ? [found, Number(key)] : found; + } // the length check makes search about 20% faster for (const nestedMod in mod.exports) if (nestedMod.length <= 3) { const nested = mod.exports[nestedMod]; - if (nested && filter(nested)) return nested; + if (nested && filter(nested)) { + return isWaitFor ? [nested, Number(key)] : nested; + } } } @@ -112,7 +117,7 @@ export const find = traceFunction("find", function find(filter: FilterFn, getDef } } - return null; + return isWaitFor ? [null, null] : null; }); /** @@ -347,8 +352,8 @@ 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 = find(filter!, true, true); - if (existing) return void callback(existing); + const [existing, id] = find(filter!, true, true); + if (existing) return void callback(existing, id); subscriptions.set(filter, callback); } From 0fb3901a185327f3de39682ea9fc6e809b935807 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 8 Mar 2023 03:01:15 -0300 Subject: [PATCH 018/147] Fix Context Menu API (#583) --- src/plugins/apiContextMenu.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/apiContextMenu.ts b/src/plugins/apiContextMenu.ts index 131c209ed..77afead84 100644 --- a/src/plugins/apiContextMenu.ts +++ b/src/plugins/apiContextMenu.ts @@ -22,6 +22,8 @@ import definePlugin from "@utils/types"; import { addListener, removeListener } from "@webpack"; function listener(exports: any, id: number) { + if (!Settings.plugins.ContextMenuAPI.enabled) return removeListener(listener); + if (typeof exports !== "object" || exports === null) return; for (const key in exports) if (key.length <= 3) { @@ -51,7 +53,7 @@ function listener(exports: any, id: number) { } } -if (Settings.plugins.ContextMenuAPI.enabled) addListener(listener); +addListener(listener); export default definePlugin({ name: "ContextMenuAPI", From 253183a16a78ce0ebc183af7e078b61eb0d4d758 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 8 Mar 2023 04:01:24 -0300 Subject: [PATCH 019/147] Fix Emote Cloner and improve ReverseImageSearch (#489) --- src/api/ContextMenu.ts | 2 +- src/plugins/apiContextMenu.ts | 4 +- src/plugins/emoteCloner.tsx | 128 +++++++++++++++-------------- src/plugins/reverseImageSearch.tsx | 76 ++++++++--------- 4 files changed, 107 insertions(+), 103 deletions(-) diff --git a/src/api/ContextMenu.ts b/src/api/ContextMenu.ts index 64671177b..9a8d7b66f 100644 --- a/src/api/ContextMenu.ts +++ b/src/api/ContextMenu.ts @@ -90,7 +90,7 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba */ export function findGroupChildrenByChildId(id: string, children: Array, itemsArray?: Array): Array | null { for (const child of children) { - if (child === null) continue; + if (child == null) continue; if (child.props?.id === id) return itemsArray ?? null; diff --git a/src/plugins/apiContextMenu.ts b/src/plugins/apiContextMenu.ts index 77afead84..69dfde4a4 100644 --- a/src/plugins/apiContextMenu.ts +++ b/src/plugins/apiContextMenu.ts @@ -37,7 +37,7 @@ function listener(exports: any, id: number) { all: true, noWarn: true, find: "navId:", - replacement: { + replacement: [{ /** Regex explanation * Use of https://blog.stevenlevithan.com/archives/mimic-atomic-groups to mimick atomic groups: (?=(...))\1 * Match ${id} and look behind it for the first match of `=`: ${id}(?=(\i)=.+?) @@ -45,7 +45,7 @@ function listener(exports: any, id: number) { */ match: RegExp(`(?=(${id}(?<=(\\i)=.+?).+?\\2\\.${key},{))\\1`, "g"), replace: "$&contextMenuApiArguments:arguments," - } + }] }); removeListener(listener); diff --git a/src/plugins/emoteCloner.tsx b/src/plugins/emoteCloner.tsx index e252fe5fb..7db5efddf 100644 --- a/src/plugins/emoteCloner.tsx +++ b/src/plugins/emoteCloner.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ -import { migratePluginSettings, Settings } from "@api/settings"; +import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { migratePluginSettings } from "@api/settings"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Devs } from "@utils/constants"; import Logger from "@utils/Logger"; import { Margins } from "@utils/margins"; -import { makeLazy } from "@utils/misc"; import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal"; import definePlugin from "@utils/types"; import { findByCodeLazy, findByPropsLazy } from "@webpack"; @@ -176,72 +176,74 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str ); } +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, args) => { + if (!args?.[0]) return; + const { favoriteableId, emoteClonerDataAlt, itemHref, itemSrc, favoriteableType } = args[0]; + + if (!emoteClonerDataAlt || favoriteableType !== "emoji") return; + + const name = emoteClonerDataAlt.match(/:(.*)(?:~\d+)?:/)?.[1]; + if (!name || !favoriteableId) return; + + const src = itemHref ?? itemSrc; + const isAnimated = new URL(src).pathname.endsWith(".gif"); + + const group = findGroupChildrenByChildId("save-image", children); + if (group && !group.some(child => child?.props?.id === "emote-cloner")) { + group.push(( + + openModal(modalProps => ( + + + + Clone {name} + + + + + + )) + } + > + + )); + } +}; + migratePluginSettings("EmoteCloner", "EmoteYoink"); export default definePlugin({ name: "EmoteCloner", description: "Adds a Clone context menu item to emotes to clone them your own server", - authors: [Devs.Ven], - dependencies: ["MenuItemDeobfuscatorAPI"], + authors: [Devs.Ven, Devs.Nuckyz], + dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"], - patches: [{ - // Literally copy pasted from ReverseImageSearch lol - find: "open-native-link", - replacement: { - match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/, - replace: "$&,$self.makeMenu(arguments[2])" - }, - - }, - // Also copy pasted from Reverse Image Search - { - // pass the target to the open link menu so we can grab its data - find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,", - predicate: makeLazy(() => !Settings.plugins.ReverseImageSearch.enabled), - noWarn: true, - replacement: { - match: /(?.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/, - replace: "$&,$.target" - } - }], - - makeMenu(htmlElement: HTMLImageElement) { - if (htmlElement?.dataset.type !== "emoji") - return null; - - const { id } = htmlElement.dataset; - const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1]; - - if (!name || !id) - return null; - - const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif"); - - return - openModal(modalProps => ( - - - - Clone {name} - - - - - - )) + patches: [ + { + find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", + replacement: { + match: /(?<=favoriteableType:\i,)(?<=(\i)\.getAttribute\("data-type"\).+?)/, + replace: (_, target) => `emoteClonerDataAlt:${target}.alt,` } - > - ; + } + ], + + start() { + addContextMenuPatch("message", messageContextMenuPatch); }, + + stop() { + removeContextMenuPatch("message", messageContextMenuPatch); + } }); diff --git a/src/plugins/reverseImageSearch.tsx b/src/plugins/reverseImageSearch.tsx index 4d9f040b1..6335fbd9f 100644 --- a/src/plugins/reverseImageSearch.tsx +++ b/src/plugins/reverseImageSearch.tsx @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { Menu } from "@webpack/common"; @@ -29,39 +30,21 @@ const Engines = { ImgOps: "https://imgops.com/start?url=" }; -export default definePlugin({ - name: "ReverseImageSearch", - description: "Adds ImageSearch to image context menus", - authors: [Devs.Ven], - dependencies: ["MenuItemDeobfuscatorAPI"], - patches: [{ - find: "open-native-link", - replacement: { - match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/, - replace: (m, src) => - `${m},Vencord.Plugins.plugins.ReverseImageSearch.makeMenu(${src}, arguments[2])` - } - }, { - // pass the target to the open link menu so we can check if it's an image - find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", - replacement: [ - { - match: /ariaLabel:\i\.Z\.Messages\.MESSAGE_ACTIONS_MENU_LABEL/, - replace: "$&,_vencordTarget:arguments[0].target" - }, - { - // var f = props.itemHref, .... MakeNativeMenu(null != f ? f : blah) - match: /(\i)=\i\.itemHref,.+?\(null!=\1\?\1:.{1,10}(?=\))/, - replace: "$&,arguments[0]._vencordTarget" - } - ] - }], +function search(src: string, engine: string) { + open(engine + encodeURIComponent(src), "_blank"); +} - makeMenu(src: string, target: HTMLElement) { - if (target && !(target instanceof HTMLImageElement) && target.attributes["data-role"]?.value !== "img") - return null; +const imageContextMenuPatch: NavContextMenuPatchCallback = (children, args) => { + if (!args?.[0]) return; + const { reverseImageSearchType, itemHref, itemSrc } = args[0]; - return ( + if (!reverseImageSearchType || reverseImageSearchType !== "img") return; + + const src = itemHref ?? itemSrc; + + const group = findGroupChildrenByChildId("save-image", children); + if (group && !group.some(child => child?.props?.id === "search-image")) { + group.push(( this.search(src, Engines[engine])} + action={() => search(src, Engines[engine])} /> ); })} @@ -82,14 +65,33 @@ export default definePlugin({ key="search-image-all" id="search-image-all" label="All" - action={() => Object.values(Engines).forEach(e => this.search(src, e))} + action={() => Object.values(Engines).forEach(e => search(src, e))} /> - ); + )); + } +}; + +export default definePlugin({ + name: "ReverseImageSearch", + description: "Adds ImageSearch to image context menus", + authors: [Devs.Ven, Devs.Nuckyz], + dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"], + patches: [ + { + find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", + replacement: { + match: /(?<=favoriteableType:\i,)(?<=(\i)\.getAttribute\("data-type"\).+?)/, + replace: (_, target) => `reverseImageSearchType:${target}.getAttribute("data-role"),` + } + } + ], + + start() { + addContextMenuPatch("message", imageContextMenuPatch); }, - // openUrl is a mangled export, so just match it in the module and pass it - search(src: string, engine: string) { - open(engine + encodeURIComponent(src), "_blank"); + stop() { + removeContextMenuPatch("message", imageContextMenuPatch); } }); From bff67885461a60ddd533b1dcec353efb66d4f1a0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 8 Mar 2023 21:19:28 -0300 Subject: [PATCH 020/147] feat(plugins): SilentMessageToggle (#586) Co-authored-by: Ven --- src/plugins/apiContextMenu.ts | 12 ++-- src/plugins/emoteCloner.tsx | 4 +- src/plugins/reverseImageSearch.tsx | 4 +- src/plugins/silentMessageToggle.tsx | 85 +++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 src/plugins/silentMessageToggle.tsx diff --git a/src/plugins/apiContextMenu.ts b/src/plugins/apiContextMenu.ts index 69dfde4a4..1874f5f05 100644 --- a/src/plugins/apiContextMenu.ts +++ b/src/plugins/apiContextMenu.ts @@ -38,13 +38,11 @@ function listener(exports: any, id: number) { noWarn: true, find: "navId:", replacement: [{ - /** Regex explanation - * Use of https://blog.stevenlevithan.com/archives/mimic-atomic-groups to mimick atomic groups: (?=(...))\1 - * Match ${id} and look behind it for the first match of `=`: ${id}(?=(\i)=.+?) - * Match rest of the code until it finds `.${key},{`: .+?\2\.${key},{ - */ - match: RegExp(`(?=(${id}(?<=(\\i)=.+?).+?\\2\\.${key},{))\\1`, "g"), - replace: "$&contextMenuApiArguments:arguments," + match: RegExp(`${id}(?<=(\\i)=.+?).+$`), + replace: (code, varName) => { + const regex = RegExp(`${key},{(?<=${varName}\\.${key},{)`, "g"); + return code.replace(regex, "$&contextMenuApiArguments:arguments,"); + } }] }); diff --git a/src/plugins/emoteCloner.tsx b/src/plugins/emoteCloner.tsx index 7db5efddf..eba77c752 100644 --- a/src/plugins/emoteCloner.tsx +++ b/src/plugins/emoteCloner.tsx @@ -233,8 +233,8 @@ export default definePlugin({ { find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", replacement: { - match: /(?<=favoriteableType:\i,)(?<=(\i)\.getAttribute\("data-type"\).+?)/, - replace: (_, target) => `emoteClonerDataAlt:${target}.alt,` + match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/, + replace: (m, target) => `${m}emoteClonerDataAlt:${target}.alt,` } } ], diff --git a/src/plugins/reverseImageSearch.tsx b/src/plugins/reverseImageSearch.tsx index 6335fbd9f..47954ba0f 100644 --- a/src/plugins/reverseImageSearch.tsx +++ b/src/plugins/reverseImageSearch.tsx @@ -81,8 +81,8 @@ export default definePlugin({ { find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", replacement: { - match: /(?<=favoriteableType:\i,)(?<=(\i)\.getAttribute\("data-type"\).+?)/, - replace: (_, target) => `reverseImageSearchType:${target}.getAttribute("data-role"),` + match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/, + replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),` } } ], diff --git a/src/plugins/silentMessageToggle.tsx b/src/plugins/silentMessageToggle.tsx new file mode 100644 index 000000000..09fb4e75c --- /dev/null +++ b/src/plugins/silentMessageToggle.tsx @@ -0,0 +1,85 @@ +/* + * 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 { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Button, ButtonLooks, ButtonWrapperClasses, React, Tooltip } from "@webpack/common"; + +function SilentMessageToggle() { + const [enabled, setEnabled] = React.useState(false); + + React.useEffect(() => { + const listener: SendListener = (_, message) => { + if (enabled) { + setEnabled(false); + if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content; + } + }; + + addPreSendListener(listener); + return () => void removePreSendListener(listener); + }, [enabled]); + + return ( + + {tooltipProps => ( + + )} + + ); +} + +export default definePlugin({ + name: "SilentMessageToggle", + authors: [Devs.Nuckyz], + description: "Adds a button to the chat bar to toggle sending a silent message.", + patches: [ + { + find: ".activeCommandOption", + replacement: { + match: /"gift"\)\);(?<=(\i)\.push.+?)/, + replace: (m, array) => `${m}${array}.push($self.SilentMessageToggle());` + } + } + ], + + SilentMessageToggle: ErrorBoundary.wrap(SilentMessageToggle, { noop: true }), +}); From d5c05d857f35f1eb101cbbd9d2b646994333d3b1 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 11 Mar 2023 00:25:32 +0100 Subject: [PATCH 021/147] Add DevOnly plugin capability --- scripts/build/common.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index c6a082dc9..16894707d 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -69,9 +69,9 @@ export const globPlugins = { const files = await readdir(`./src/${dir}`); for (const file of files) { if (file.startsWith(".")) continue; - if (file === "index.ts") { - continue; - } + if (file === "index.ts") continue; + if (!watch && (file.endsWith(".dev.ts") || file.endsWith(".dev.tsx"))) continue; + const mod = `p${i}`; code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`; plugins += `[${mod}.name]:${mod},\n`; From 983414d0241e1a2b691866d74dca632c9fb9576a Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 11 Mar 2023 00:25:49 +0100 Subject: [PATCH 022/147] Add DevCompanion plugin (https://github.com/Vencord/Companion) --- src/plugins/devCompanion.dev.tsx | 250 +++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 src/plugins/devCompanion.dev.tsx diff --git a/src/plugins/devCompanion.dev.tsx b/src/plugins/devCompanion.dev.tsx new file mode 100644 index 000000000..eaf13b7d2 --- /dev/null +++ b/src/plugins/devCompanion.dev.tsx @@ -0,0 +1,250 @@ +/* + * 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 { addContextMenuPatch } from "@api/ContextMenu"; +import { showNotification } from "@api/Notifications"; +import { Devs } from "@utils/constants"; +import Logger from "@utils/Logger"; +import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; +import definePlugin from "@utils/types"; +import { filters, findAll, search } from "@webpack"; +import { Menu } from "@webpack/common"; + +const PORT = 8485; +const NAV_ID = "dev-companion-reconnect"; + +const logger = new Logger("DevCompanion"); + +let socket: WebSocket | undefined; + +type Node = StringNode | RegexNode | FunctionNode; + +interface StringNode { + type: "string"; + value: string; +} + +interface RegexNode { + type: "regex"; + value: { + pattern: string; + flags: string; + }; +} + +interface FunctionNode { + type: "function"; + value: string; +} + +interface PatchData { + find: string; + replacement: { + match: StringNode | RegexNode; + replace: StringNode | FunctionNode; + }[]; +} + +interface FindData { + type: string; + args: Array; +} + +function parseNode(node: Node) { + switch (node.type) { + case "string": + return node.value; + case "regex": + return new RegExp(node.value.pattern, node.value.flags); + case "function": + // We LOVE remote code execution + // Safety: This comes from localhost only, which actually means we have less permissions than the source, + // since we're running in the browser sandbox, whereas the sender has host access + return (0, eval)(node.value); + default: + throw new Error("Unknown Node Type " + (node as any).type); + } +} + +function initWs(isManual = false) { + let wasConnected = isManual; + let hasErrored = false; + const ws = socket = new WebSocket(`ws://localhost:${PORT}`); + + ws.addEventListener("open", () => { + wasConnected = true; + + logger.info("Connected to WebSocket"); + + showNotification({ + title: "Dev Companion Connected", + body: "Connected to WebSocket" + }); + }); + + ws.addEventListener("error", e => { + if (!wasConnected) return; + + hasErrored = true; + + logger.error("Dev Companion Error:", e); + + showNotification({ + title: "Dev Companion Error", + body: (e as ErrorEvent).message || "No Error Message", + color: "var(--status-danger, red)" + }); + }); + + ws.addEventListener("close", e => { + if (!wasConnected && !hasErrored) return; + + logger.info("Dev Companion Disconnected:", e.code, e.reason); + + showNotification({ + title: "Dev Companion Disconnected", + body: e.reason || "No Reason provided", + color: "var(--status-danger, red)" + }); + }); + + ws.addEventListener("message", e => { + try { + var { nonce, type, data } = JSON.parse(e.data); + } catch (err) { + logger.error("Invalid JSON:", err, "\n" + e.data); + return; + } + + function reply(error?: string) { + const data = { nonce, ok: !error } as Record; + if (error) data.error = error; + + ws.send(JSON.stringify(data)); + } + + logger.info("Received Message:", type, "\n", data); + + switch (type) { + case "testPatch": { + const { find, replacement } = data as PatchData; + + const candidates = search(find); + const keys = Object.keys(candidates); + if (keys.length !== 1) + return reply("Expected exactly one 'find' matches, found " + keys.length); + + let src = String(candidates[keys[0]]); + + let i = 0; + + for (const { match, replace } of replacement) { + i++; + + try { + const matcher = canonicalizeMatch(parseNode(match)); + const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName"); + + const newSource = src.replace(matcher, replacement as string); + + if (src === newSource) throw "Had no effect"; + Function(newSource); + + src = newSource; + } catch (err) { + return reply(`Replacement ${i} failed: ${err}`); + } + } + + reply(); + break; + } + case "testFind": { + const { type, args } = data as FindData; + try { + var parsedArgs = args.map(parseNode); + } catch (err) { + return reply("Failed to parse args: " + err); + } + + try { + let results: any[]; + switch (type.replace("find", "").replace("Lazy", "")) { + case "": + results = findAll(parsedArgs[0]); + break; + case "ByProps": + results = findAll(filters.byProps(...parsedArgs)); + break; + case "Store": + results = findAll(filters.byStoreName(parsedArgs[0])); + break; + case "ByCode": + results = findAll(filters.byCode(...parsedArgs)); + break; + case "ModuleId": + results = Object.keys(search(parsedArgs[0])); + break; + default: + return reply("Unknown Find Type " + type); + } + + if (results.length === 0) throw "No results"; + if (results.length > 1) throw "Found more than one result! Make this filter more specific"; + } catch (err) { + return reply("Failed to find: " + err); + } + + reply(); + break; + } + default: + reply("Unknown Type " + type); + break; + } + }); +} + +export default definePlugin({ + name: "DevCompanion", + description: "Dev Companion Plugin", + authors: [Devs.Ven], + + start() { + initWs(); + addContextMenuPatch("user-settings-cog", kids => { + if (kids.some(k => k?.props?.id === NAV_ID)) return; + + kids.unshift( + { + socket?.close(1000, "Reconnecting"); + initWs(true); + }} + /> + ); + }); + }, + + stop() { + socket?.close(1000, "Plugin Stopped"); + socket = void 0; + } +}); From 990adf752767de7e6c45270c5fcae8d1a467f325 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 11 Mar 2023 00:27:02 +0100 Subject: [PATCH 023/147] Fix casing in filename --- src/plugins/{FixInbox.tsx => fixInbox.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/plugins/{FixInbox.tsx => fixInbox.tsx} (100%) diff --git a/src/plugins/FixInbox.tsx b/src/plugins/fixInbox.tsx similarity index 100% rename from src/plugins/FixInbox.tsx rename to src/plugins/fixInbox.tsx From 19c762f9c10e933be8b568be9738d3abc84b995e Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 11 Mar 2023 00:28:27 +0100 Subject: [PATCH 024/147] DevCompanion: Fix Deps --- src/plugins/devCompanion.dev.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/devCompanion.dev.tsx b/src/plugins/devCompanion.dev.tsx index eaf13b7d2..1dbf4ca68 100644 --- a/src/plugins/devCompanion.dev.tsx +++ b/src/plugins/devCompanion.dev.tsx @@ -224,6 +224,7 @@ export default definePlugin({ name: "DevCompanion", description: "Dev Companion Plugin", authors: [Devs.Ven], + dependencies: ["ContextMenuAPI"], start() { initWs(); From 3b945b87b8dc0901dd3f93230afdbb70b5ccd8c8 Mon Sep 17 00:00:00 2001 From: Ven Date: Sat, 11 Mar 2023 12:26:54 +0100 Subject: [PATCH 025/147] Delete src/plugins/reviewDB directory Api owner refusing to properly moderate hate speech and related illegal / ToS infringing content --- src/plugins/reviewDB/Utils/ReviewDBAPI.ts | 95 ------------- src/plugins/reviewDB/Utils/Utils.tsx | 95 ------------- .../reviewDB/components/MessageButton.tsx | 43 ------ .../reviewDB/components/ReviewBadge.tsx | 45 ------- .../reviewDB/components/ReviewComponent.tsx | 125 ------------------ .../reviewDB/components/ReviewsView.tsx | 94 ------------- src/plugins/reviewDB/entities/Badge.ts | 26 ---- src/plugins/reviewDB/entities/Review.ts | 30 ----- src/plugins/reviewDB/index.tsx | 80 ----------- 9 files changed, 633 deletions(-) delete mode 100644 src/plugins/reviewDB/Utils/ReviewDBAPI.ts delete mode 100644 src/plugins/reviewDB/Utils/Utils.tsx delete mode 100644 src/plugins/reviewDB/components/MessageButton.tsx delete mode 100644 src/plugins/reviewDB/components/ReviewBadge.tsx delete mode 100644 src/plugins/reviewDB/components/ReviewComponent.tsx delete mode 100644 src/plugins/reviewDB/components/ReviewsView.tsx delete mode 100644 src/plugins/reviewDB/entities/Badge.ts delete mode 100644 src/plugins/reviewDB/entities/Review.ts delete mode 100644 src/plugins/reviewDB/index.tsx diff --git a/src/plugins/reviewDB/Utils/ReviewDBAPI.ts b/src/plugins/reviewDB/Utils/ReviewDBAPI.ts deleted file mode 100644 index a4c8dbfdb..000000000 --- a/src/plugins/reviewDB/Utils/ReviewDBAPI.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 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 { Settings } from "@api/settings"; - -import { Review } from "../entities/Review"; -import { authorize, showToast } from "./Utils"; - -const API_URL = "https://manti.vendicated.dev"; - -const getToken = () => Settings.plugins.ReviewDB.token; - -enum Response { - "Added your review" = 0, - "Updated your review" = 1, - "Error" = 2, -} - -export async function getReviews(id: string): Promise { - const res = await fetch(API_URL + "/getUserReviews?snowflakeFormat=string&discordid=" + id); - return await res.json() as Review[]; -} - -export async function addReview(review: any): Promise { - review.token = getToken(); - - if (!review.token) { - showToast("Please authorize to add a review."); - authorize(); - return Response.Error; - } - - return fetch(API_URL + "/addUserReview", { - method: "POST", - body: JSON.stringify(review), - headers: { - "Content-Type": "application/json", - } - }) - .then(r => r.text()) - .then(res => { - showToast(res); - return Response[res] ?? Response.Error; - }); -} - -export function deleteReview(id: number): Promise { - return fetch(API_URL + "/deleteReview", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - Accept: "application/json", - }), - body: JSON.stringify({ - token: getToken(), - reviewid: id - }) - }).then(r => r.json()); -} - -export async function reportReview(id: number) { - const res = await fetch(API_URL + "/reportReview", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - Accept: "application/json", - }), - body: JSON.stringify({ - reviewid: id, - token: getToken() - }) - }); - showToast(await res.text()); -} - -export function getLastReviewID(id: string): Promise { - return fetch(API_URL + "/getLastReviewID?discordid=" + id) - .then(r => r.text()) - .then(Number); -} diff --git a/src/plugins/reviewDB/Utils/Utils.tsx b/src/plugins/reviewDB/Utils/Utils.tsx deleted file mode 100644 index 79d768c8b..000000000 --- a/src/plugins/reviewDB/Utils/Utils.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 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 { Settings } from "@api/settings"; -import { Devs } from "@utils/constants"; -import Logger from "@utils/Logger"; -import { openModal } from "@utils/modal"; -import { findByProps } from "@webpack"; -import { FluxDispatcher, React, SelectedChannelStore, Toasts, UserUtils } from "@webpack/common"; - -import { Review } from "../entities/Review"; - -export async function openUserProfileModal(userId: string) { - await UserUtils.fetchUser(userId); - - await FluxDispatcher.dispatch({ - type: "USER_PROFILE_MODAL_OPEN", - userId, - channelId: SelectedChannelStore.getChannelId(), - analyticsLocation: "Explosive Hotel" - }); -} - -export function authorize(callback?: any) { - const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal"); - - openModal((props: any) => - { - try { - const url = new URL(u); - url.searchParams.append("returnType", "json"); - url.searchParams.append("clientMod", "vencord"); - const res = await fetch(url, { - headers: new Headers({ Accept: "application/json" }) - }); - const { token, status } = await res.json(); - if (status === 0) { - Settings.plugins.ReviewDB.token = token; - showToast("Successfully logged in!"); - callback?.(); - } else if (res.status === 1) { - showToast("An Error occurred while logging in."); - } - } catch (e) { - new Logger("ReviewDB").error("Failed to authorise", e); - } - }} - /> - ); -} - -export function showToast(text: string) { - Toasts.show({ - type: Toasts.Type.MESSAGE, - message: text, - id: Toasts.genId(), - options: { - position: Toasts.Position.BOTTOM - }, - }); -} - -export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); - -export function canDeleteReview(review: Review, userId: string) { - if (review.senderdiscordid === userId) return true; - - const myId = BigInt(userId); - return myId === Devs.mantikafasi.id || - myId === Devs.Ven.id || - myId === Devs.rushii.id; -} diff --git a/src/plugins/reviewDB/components/MessageButton.tsx b/src/plugins/reviewDB/components/MessageButton.tsx deleted file mode 100644 index c334ddd31..000000000 --- a/src/plugins/reviewDB/components/MessageButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 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 { classes, LazyComponent } from "@utils/misc"; -import { findByProps } from "@webpack"; - -export default LazyComponent(() => { - const { button, dangerous } = findByProps("button", "wrapper", "disabled"); - - return function MessageButton(props) { - return props.type === "delete" - ? ( -
- - - - -
- ) - : ( -
props.callback()}> - - - -
- ); - }; -}); diff --git a/src/plugins/reviewDB/components/ReviewBadge.tsx b/src/plugins/reviewDB/components/ReviewBadge.tsx deleted file mode 100644 index 4a3c0c4c1..000000000 --- a/src/plugins/reviewDB/components/ReviewBadge.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 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 { MaskedLinkStore, Tooltip } from "@webpack/common"; - -import { Badge } from "../entities/Badge"; - -export default function ReviewBadge(badge: Badge) { - return ( - - {({ onMouseEnter, onMouseLeave }) => ( - {badge.badge_description} - MaskedLinkStore.openUntrustedLink({ - href: badge.redirect_url, - }) - } - /> - )} - - ); -} diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx deleted file mode 100644 index ddb49223d..000000000 --- a/src/plugins/reviewDB/components/ReviewComponent.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 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 { classes, LazyComponent } from "@utils/misc"; -import { filters, findBulk } from "@webpack"; -import { Alerts, UserStore } from "@webpack/common"; - -import { Review } from "../entities/Review"; -import { deleteReview, reportReview } from "../Utils/ReviewDBAPI"; -import { canDeleteReview, openUserProfileModal, showToast } from "../Utils/Utils"; -import MessageButton from "./MessageButton"; -import ReviewBadge from "./ReviewBadge"; - -export default LazyComponent(() => { - // this is terrible, blame mantika - const p = filters.byProps; - const [ - { cozyMessage, buttons, message, groupStart }, - { container, isHeader }, - { avatar, clickable, username, messageContent, wrapper, cozy }, - { contents }, - buttonClasses, - { defaultColor } - ] = findBulk( - p("cozyMessage"), - p("container", "isHeader"), - p("avatar", "zalgo"), - p("contents"), - p("button", "wrapper", "disabled"), - p("defaultColor") - ); - - return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) { - function openModal() { - openUserProfileModal(review.senderdiscordid); - } - - function delReview() { - Alerts.show({ - title: "Are you sure?", - body: "Do you really want to delete this review?", - confirmText: "Delete", - cancelText: "Nevermind", - onConfirm: () => { - deleteReview(review.id).then(res => { - if (res.successful) { - refetch(); - } - showToast(res.message); - }); - } - }); - } - - function reportRev() { - Alerts.show({ - title: "Are you sure?", - body: "Do you really you want to report this review?", - confirmText: "Report", - cancelText: "Nevermind", - // confirmColor: "red", this just adds a class name and breaks the submit button guh - onConfirm: () => reportReview(review.id) - }); - } - - return ( -
- -
- - openModal()} - > - {review.username} - - {review.badges.map(badge => )} -

- {review.comment} -

-
-
- - {canDeleteReview(review, UserStore.getCurrentUser().id) && ( - - )} -
-
-
-
- ); - }; -}); diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx deleted file mode 100644 index c62065f7e..000000000 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 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 { classes, useAwaiter } from "@utils/misc"; -import { findLazy } from "@webpack"; -import { Forms, React, Text, UserStore } from "@webpack/common"; -import type { KeyboardEvent } from "react"; - -import { addReview, getReviews } from "../Utils/ReviewDBAPI"; -import ReviewComponent from "./ReviewComponent"; - -const Classes = findLazy(m => typeof m.textarea === "string"); - -export default function ReviewsView({ userId }: { userId: string; }) { - const [refetchCount, setRefetchCount] = React.useState(0); - const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), { - fallbackValue: [], - deps: [refetchCount], - }); - const username = UserStore.getUser(userId)?.username ?? ""; - - const dirtyRefetch = () => setRefetchCount(refetchCount + 1); - - if (isLoading) return null; - - function onKeyPress({ key, target }: KeyboardEvent) { - if (key === "Enter") { - addReview({ - userid: userId, - comment: (target as HTMLInputElement).value, - star: -1 - }).then(res => { - if (res === 0 || res === 1) { - (target as HTMLInputElement).value = ""; // clear the input - dirtyRefetch(); - } - }); - } - } - - return ( -
- - User Reviews - - {reviews?.map(review => - - )} - {reviews?.length === 0 && ( - - Looks like nobody reviewed this user yet. You could be the first! - - )} -