aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/plugins/messageLinkEmbeds.tsx491
1 files changed, 265 insertions, 226 deletions
diff --git a/src/plugins/messageLinkEmbeds.tsx b/src/plugins/messageLinkEmbeds.tsx
index 75b99e1..e7b3f72 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,107 +38,153 @@ import {
} from "@webpack/common";
import { Channel, Guild, Message } from "discord-types/general";
-let messageCache: { [id: string]: { message?: Message, fetched: boolean; }; } = {};
+const messageCache = new Map<string, {
+ message?: Message;
+ fetched: boolean;
+}>();
-let AutomodEmbed: React.ComponentType<any>,
- Embed: React.ComponentType<any>,
- ChannelMessage: React.ComponentType<any>,
- Endpoints: Record<string, any>;
+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");
+let AutoModEmbed: React.ComponentType<any> = () => null;
+
+const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
+const tenorRegex = /https:\/\/(?:www.)?tenor\.com/;
+
+interface Attachment {
+ height: number;
+ width: number;
+ url: string;
+ proxyURL?: string;
+}
+
+interface MessageEmbedProps {
+ message: Message;
+ channel: Channel;
+ guildID: string;
+}
+
const messageFetchQueue = new Queue();
-async function fetchMessage(channelID: string, messageID: string): Promise<Message | void> {
- if (messageID in messageCache && !messageCache[messageID].fetched) return Promise.resolve();
- if (messageCache[messageID]?.fetched) return Promise.resolve(messageCache[messageID].message);
- messageCache[messageID] = { fetched: false };
+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: () =>
+ <Button onClick={() => messageCache.clear()}>
+ Clear the linked message cache
+ </Button>
+ }
+});
+
+
+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: Endpoints.MESSAGES(channelID),
+ url: `/channels/${channelID}/messages`,
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,
+ }).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 Promise.resolve(message);
-}
+ });
-interface Attachment {
- height: number;
- width: number;
- url: string;
- proxyURL?: string;
+ return message;
}
-const isTenorGif = /https:\/\/(?:www.)?tenor\.com/;
+
function getImages(message: Message): Attachment[] {
const attachments: Attachment[] = [];
- message.attachments?.forEach(a => {
- 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!)) {
+
+ for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) {
+ if (content_type?.startsWith("image/"))
attachments.push({
- height: e.thumbnail!.height,
- width: e.thumbnail!.width,
- url: e.url!
+ 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;
}
-const noContent = (attachments: number, embeds: number): string => {
+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.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;
+ 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;
}
-const computeWidthAndHeight = (width: number, height: number) => {
- const maxWidth = 400, maxHeight = 300;
- let newWidth: number, newHeight: number;
+function computeWidthAndHeight(width: number, height: number) {
+ const maxWidth = 400;
+ const maxHeight = 300;
+
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));
+ const adjustedWidth = Math.min(width, maxWidth);
+ return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) };
}
- return { width: newWidth, height: newHeight };
-};
-interface MessageEmbedProps {
- message: Message;
- channel: Channel;
- guildID: string;
+ const adjustedHeight = Math.min(height, maxHeight);
+ return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight };
}
function withEmbeddedBy(message: Message, embeddedBy: string[]) {
@@ -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))
+ ? <AutomodEmbedAccessory {...messageProps} />
+ : <ChannelMessageEmbedAccessory {...messageProps} />
+ );
+ }
+
+ 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 <Embed
+ embed={{
+ rawDescription: "",
+ color: "var(--background-secondary)",
+ author: {
+ name: <Text variant="text-xs/medium" tag="span">
+ <span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>,
+ {isDM
+ ? Parser.parse(`<@${dmReceiver.id}>`)
+ : Parser.parse(`<#${channel.id}>`)
+ }
+ </Text>,
+ 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={() => (
+ <div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
+ <ChannelMessage
+ id={`message-link-embeds-${message.id}`}
+ message={message}
+ channel={channel}
+ subscribeToComponentDispatch={false}
+ />
+ </div>
+ )}
+ />;
+}
+
+function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
+ const { message, channel, guildID } = props;
+
+ const isDM = guildID === "@me";
+ const images = getImages(message);
+ const { parse } = Parser;
+
+ return <AutoModEmbed
+ channel={channel}
+ childrenAccessories={
+ <Text color="text-muted" variant="text-xs/medium" tag="span">
+ {isDM
+ ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
+ : parse(`<#${channel.id}>`)
+ },
+ <span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
+ </Text>
+ }
+ 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 (
+ <div>
+ <img src={a.url} width={width} height={height} />
+ </div>
+ );
+ })}
+ </>
+ }
+ 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: () =>
- <Button onClick={() => messageCache = {}}>
- Clear the linked message cache
- </Button>
- }
- },
- start() {
- addAccessory("messageLinkEmbed", props => this.messageEmbedAccessory(props), 4 /* just above rich embeds*/);
+ set AutoModEmbed(e: any) {
+ AutoModEmbed = e;
},
- messageLinkRegex: /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,19}|@me)\/(\d{17,19})\/(\d{17,19})/g,
-
- messageEmbedAccessory(props) {
- const { message }: { message: Message; } = props;
- // @ts-ignore
- const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
+ settings,
- const accessories = [] as (JSX.Element | null)[];
+ start() {
+ addAccessory("messageLinkEmbed", props => {
+ if (!messageLinkRegex.test(props.message.content))
+ return null;
- let match = null as RegExpMatchArray | null;
- while ((match = this.messageLinkRegex.exec(message.content!)) !== null) {
- const [_, guildID, channelID, messageID] = match;
- if (embeddedBy.includes(messageID)) {
- continue;
- }
+ // need to reset the regex because it's global
+ messageLinkRegex.lastIndex = 0;
- const linkedChannel = ChannelStore.getChannel(channelID);
- if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) {
- continue;
- }
-
- let linkedMessage = messageCache[messageID]?.message;
- if (!linkedMessage) {
- linkedMessage ??= MessageStore.getMessage(channelID, messageID);
- if (linkedMessage) messageCache[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.plugins[this.name].automodEmbeds;
- accessories.push(
- type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
- ? this.automodEmbedAccessory(messageProps)
- : this.channelMessageEmbedAccessory(messageProps)
+ return (
+ <ErrorBoundary>
+ <MessageEmbedAccessory message={props.message} />
+ </ErrorBoundary>
);
- }
- 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 <Embed
- embed={{
- rawDescription: "",
- color: "var(--background-secondary)",
- author: {
- name: <Text variant="text-xs/medium" tag="span">
- {[
- <span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>,
- ...(isDM
- ? Parser.parse(`<@${dmReceiver.id}>`)
- : Parser.parse(`<#${channel.id}>`)
- )
- ]}
- </Text>,
- 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 <div key={message.id} className={classNames.join(" ")}>
- <ChannelMessage
- id={`message-link-embeds-${message.id}`}
- message={message}
- channel={channel}
- subscribeToComponentDispatch={false}
- />
- </div >;
- }}
- />;
- },
-
- automodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
- const { message, channel, guildID } = props;
-
- const isDM = guildID === "@me";
- const images = getImages(message);
- const { parse } = Parser;
-
- return <AutomodEmbed
- channel={channel}
- childrenAccessories={<Text color="text-muted" variant="text-xs/medium" tag="span">
- {[
- ...(isDM ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) : parse(`<#${channel.id}>`)),
- <span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
- ]}
- </Text>}
- compact={false}
- content={[
- ...(message.content || !(message.attachments.length > images.length)
- ? parse(message.content)
- : [noContent(message.attachments.length, message.embeds.length)]
- ),
- ...(images.map<JSX.Element>(a => {
- const { width, height } = computeWidthAndHeight(a.width, a.height);
- return <div><img src={a.url} width={width} height={height} /></div>;
- }
- ))
- ]}
- hideTimestamp={false}
- message={message}
- _messageEmbed="automod"
- />;
+ }, 4 /* just above rich embeds */);
},
});