import { For, Match, Show, Switch, createSignal, onMount } from "solid-js"; import { useLingui } from "@lingui-solid/solid/macro"; import { ImageEmbed, Message as MessageInterface, WebsiteEmbed } from "stoat.js"; import { cva } from "styled-system/css"; import { styled } from "styled-system/jsx"; import { decodeTime } from "ulid"; import { useClient } from "@revolt/client"; import { useTime } from "@revolt/i18n"; import { Markdown } from "@revolt/markdown"; import { useState } from "@revolt/state"; import { Attachment, Avatar, Embed, MessageContainer, MessageReply, Reactions, SystemMessage, SystemMessageIcon, Tooltip, Username, } from "@revolt/ui"; import { Symbol } from "@revolt/ui/components/utils/Symbol"; import { MessageContextMenu } from "../../../menus/MessageContextMenu"; import { floatingUserMenus, floatingUserMenusFromMessage, } from "../../../menus/UserContextMenu"; import { EditMessage } from "./EditMessage"; /** * Regex for matching URLs */ const RE_URL = /[(http(s)?)://(www.)?a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/; interface Props { /** * Message */ message: MessageInterface; /** * Whether this is the tail of another message */ tail?: boolean; /** * Whether to highlight this message */ highlight?: boolean; /** * Whether to replace content with editor */ editing?: boolean; /** * Whether this message is a link */ isLink?: boolean; } /** * Render a Message with or without a tail */ export function Message(props: Props) { const dayjs = useTime(); const state = useState(); const { t } = useLingui(); const client = useClient(); const [isHovering, setIsHovering] = createSignal(false); /** * Determine whether this message only contains a GIF */ const isOnlyGIF = () => { if ( !props.message.embeds || props.message.embeds.length !== 1 || !props.message.content || props.message.content.replace(RE_URL, "").length ) { return false; } const embed = props.message.embeds[0]; if (embed.type === "Image") { return (embed as ImageEmbed).url?.startsWith("https://static.klipy.com"); } if (embed.type === "Website") { const websiteEmbed = embed as WebsiteEmbed; const gifProviders = ["https://tenor.com", "https://klipy.com"]; return ( websiteEmbed.specialContent?.type === "GIF" || gifProviders.some((provider) => websiteEmbed.originalUrl?.startsWith(provider), ) ); } return false; }; /** * React with an emoji * @param emoji Emoji */ const react = (emoji: string) => props.message.react(emoji); /** * Remove emoji reaction * @param emoji Emoji */ const unreact = (emoji: string) => props.message.unreact(emoji); return ( } avatar={
} contextMenu={() => } timestamp={props.message.createdAt} edited={props.message.editedAt} mentioned={props.message.mentioned} highlight={props.highlight} editing={props.editing} isLink={props.isLink} tail={props.tail || state.settings.getValue("appearance:compact_mode")} header={ {(reply_id) => { /** * Signal the actual message */ const message = () => client().messages.get(reply_id); onMount(() => { if (!message()) { props.message.channel!.fetchMessage(reply_id); } }); return ( ); }} } info={ }> link brightness_alert smart_toy cloud notifications_off spa spa {/* placeholder · */} } compact={ !!props.message.systemMessage || state.settings.getValue("appearance:compact_mode") } infoMatch={ } > user ? floatingUserMenus( user!, // TODO: try to fetch on demand member props.message.server?.getMember(user!.id), ) : {} } isServer={!!props.message.server} /> {(attachment) => ( )} {(embed) => } >} interactions={props.message.interactions} userId={client().user!.id} addReaction={react} removeReaction={unreact} sendGIF={(content) => props.message?.channel?.sendMessage({ content, replies: [{ id: props.message.id, mention: true }], }) } />
); } /** * New user indicator */ const NewUser = styled("div", { base: { fill: "var(--md-sys-color-primary)", }, }); /** * Avatar container */ const avatarContainer = cva({ base: { height: "fit-content", borderRadius: "var(--borderRadius-circle)", }, }); /** * Break all text and prevent overflow from math blocks */ const BreakText = styled("div", { base: { wordBreak: "break-word", "& .math": { overflowX: "auto", overflowY: "hidden", maxHeight: "100vh", }, }, });