// SPDX-License-Identifier: AGPL-3.0-or-later import {GroupDMAvatar} from '@app/features/app/components/shared/GroupDMAvatar'; import {SpoilerOverlay} from '@app/features/app/components/shared/SpoilerOverlay'; import {Attachment} from '@app/features/channel/components/embeds/attachments/Attachment'; import {AttachmentMosaic} from '@app/features/channel/components/embeds/attachments/AttachmentMosaic'; import {Embed} from '@app/features/channel/components/embeds/ChannelEmbed'; import {GiftEmbed} from '@app/features/channel/components/GiftEmbed'; import {InviteEmbed} from '@app/features/channel/components/InviteEmbed'; import {getAttachmentRenderingState} from '@app/features/channel/components/MessageAttachmentStateUtils'; import styles from '@app/features/channel/components/MessageAttachments.module.css'; import {MessageReactions} from '@app/features/channel/components/MessageReactions'; import {useMessageViewContext} from '@app/features/channel/components/MessageViewContext'; import {ThemeEmbed} from '@app/features/channel/components/ThemeEmbed'; import {TimestampWithTooltip} from '@app/features/channel/components/TimestampWithTooltip'; import type {Channel} from '@app/features/channel/models/Channel'; import {useStickerAnimation} from '@app/features/emoji/hooks/useStickerAnimation'; import Sticker from '@app/features/emoji/state/EmojiSticker'; import * as GiftCodeUtils from '@app/features/gift/utils/GiftCodeUtils'; import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon'; import Guilds from '@app/features/guild/state/Guilds'; import * as InviteUtils from '@app/features/invite/utils/InviteUtils'; import {SafeMarkdown} from '@app/features/messaging/components/markdown'; import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes'; import {useMatureMedia} from '@app/features/messaging/hooks/useMatureMedia'; import {useMessageReactions as useMessageReactionsSnapshot} from '@app/features/messaging/hooks/useMessageReactionStore'; import type {Message} from '@app/features/messaging/models/MessagingMessage'; import {extractEmbeddableCodeLinkContent} from '@app/features/messaging/utils/EmbeddableCodeLinkContent'; import {useForwardedMessageContext} from '@app/features/messaging/utils/ForwardedMessageUtils'; import {buildMessageSnapshotCopyText} from '@app/features/messaging/utils/MessageCopyTextUtils'; import {goToMessage} from '@app/features/messaging/utils/MessageNavigator'; import {canonicalizeMediaUrl, useSpoilerState} from '@app/features/messaging/utils/SpoilerUtils'; import markupStyles from '@app/features/theme/styles/Markup.module.css'; import matureStyles from '@app/features/theme/styles/MatureBlur.module.css'; import messageStyles from '@app/features/theme/styles/Message.module.css'; import * as ThemeUtils from '@app/features/theme/utils/ThemeUtils'; import {StickerInlineMenuItems} from '@app/features/ui/action_menu/items/StickerContextMenuItems'; import {MessageContextMenu} from '@app/features/ui/action_menu/MessageContextMenu'; import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands'; import {Avatar} from '@app/features/ui/components/Avatar'; import FocusRing from '@app/features/ui/focus_ring/FocusRing'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import UserSettings from '@app/features/user/state/UserSettings'; import * as AvatarUtils from '@app/features/user/utils/AvatarUtils'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; import type {MessageEmbed} from '@fluxer/schema/src/domains/message/EmbedSchemas'; import type { MessageAttachment, MessageSnapshot, MessageStickerItem, } from '@fluxer/schema/src/domains/message/MessageResponseSchemas'; import {Trans, useLingui} from '@lingui/react/macro'; import {ArrowBendUpRightIcon, CaretRightIcon, HashIcon, NotePencilIcon, SpeakerHighIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import type React from 'react'; import {useCallback, useMemo} from 'react'; interface SpoileredCodeLinkMatch { code: string; matchedText: string; } type SpoileredCodeLinkMap = ReadonlyMap>; const EMPTY_SPOILER_KEYS: ReadonlyArray = Object.freeze([]); function getCodeLinkSpoilerSyncKeys(match: SpoileredCodeLinkMatch, keyPrefix: string): Array { const canonicalUrl = canonicalizeMediaUrl(match.matchedText) ?? canonicalizeMediaUrl(`https://${match.matchedText}`); return canonicalUrl ? [`${keyPrefix}:${match.code}`, canonicalUrl] : [`${keyPrefix}:${match.code}`]; } function buildSpoileredCodeLinkMap( matches: ReadonlyArray, keyPrefix: string, ): SpoileredCodeLinkMap { const linksByCode = new Map>(); for (const match of matches) { const existing = linksByCode.get(match.code) ?? []; for (const key of getCodeLinkSpoilerSyncKeys(match, keyPrefix)) { if (!existing.includes(key)) { existing.push(key); } } linksByCode.set(match.code, existing); } return linksByCode; } interface SpoileredUrlEmbedProps { channelId: string; spoilerKeys?: ReadonlyArray; children: React.ReactElement; } const SpoileredUrlEmbed = observer(function SpoileredUrlEmbed({ channelId, spoilerKeys, children, }: SpoileredUrlEmbedProps) { const syncKeys = spoilerKeys ?? EMPTY_SPOILER_KEYS; const isSpoiler = syncKeys.length > 0; const {hidden, reveal} = useSpoilerState(isSpoiler, channelId, syncKeys); if (!isSpoiler) { return children; } return ( ); }); const ForwardedFromSource = observer(({message}: {message: Message}) => { const {sourceChannel, sourceGuild, sourceUser, hasAccessToSource, displayName} = useForwardedMessageContext(message); const handleJumpToOriginal = useCallback(() => { if (message.messageReference && sourceChannel) { goToMessage(message.messageReference.channel_id, message.messageReference.message_id, { returnToMessageId: message.id, returnChannelId: message.channelId, }); } }, [message.id, message.messageReference, sourceChannel]); const renderChannelIcon = useCallback(() => { if (!sourceChannel) return null; const iconSize = 16; if (sourceChannel.type === ChannelTypes.DM_PERSONAL_NOTES) { return ( ); } if (sourceChannel.type === ChannelTypes.DM && sourceUser) { return (
); } if (sourceChannel.type === ChannelTypes.GROUP_DM) { return (
); } if (sourceChannel.type === ChannelTypes.GUILD_VOICE) { return ( ); } return ( ); }, [sourceChannel, sourceUser]); if (!hasAccessToSource || !sourceChannel || !displayName || !message.messageReference) { return null; } if ( sourceChannel.type === ChannelTypes.DM || sourceChannel.type === ChannelTypes.GROUP_DM || sourceChannel.type === ChannelTypes.DM_PERSONAL_NOTES ) { return ( ); } if (sourceGuild) { return ( ); } return null; }); interface ForwardedMessageContentProps { message: Message; snapshot: MessageSnapshot; shouldAnimate: boolean; interactionHandlers: Record; onDelete?: (bypassConfirm?: boolean) => void; } export const ForwardedMessageContent = observer( ({message, snapshot, shouldAnimate, interactionHandlers, onDelete}: ForwardedMessageContentProps) => { const {i18n} = useLingui(); const snapshotIndex = 0; const snapshotEditedTimestamp = snapshot.edited_timestamp ? new Date(snapshot.edited_timestamp) : null; const copyText = useMemo( () => buildMessageSnapshotCopyText(snapshot, { channelId: message.channelId, messageId: message.id, i18n, }), [i18n.locale, message.channelId, message.id, snapshot], ); return (
Forwarded
{snapshot.content && (
{snapshotEditedTimestamp && ( {' '} (edited) )}
)} {snapshot.attachments && snapshot.attachments.length > 0 && (
{(() => { const {enrichedAttachments, mediaAttachments, shouldUseMosaic} = getAttachmentRenderingState( snapshot.attachments, ); return ( <> {shouldUseMosaic && ( )} {enrichedAttachments.map((attachment: MessageAttachment) => ( ))} ); })()}
)} {snapshot.embeds && snapshot.embeds.length > 0 && UserSettings.getRenderEmbeds() && (
{snapshot.embeds.map((embed: MessageEmbed, index: number) => { const embedKey = `${embed.id}-${index}`; return ( ); })}
)} {snapshot.stickers && snapshot.stickers.length > 0 && (
{snapshot.stickers.map((sticker: MessageStickerItem) => ( ))}
)}
); }, ); interface StickerItemProps { sticker: MessageStickerItem; message: Message; shouldAnimate: boolean; interactionHandlers: Record; sourceChannel?: Channel | null; handleDelete?: (bypassConfirm?: boolean) => void; } const StickerItem = observer( ({sticker, message, shouldAnimate, interactionHandlers, sourceChannel, handleDelete}: StickerItemProps) => { const stickerUrl = AvatarUtils.getStickerURL({ id: sticker.id, animated: shouldAnimate, size: 320, }); const stickerRecord = Sticker.getStickerById(sticker.id); const guild = stickerRecord?.guildId ? Guilds.getGuild(stickerRecord.guildId) : null; const isMature = !!stickerRecord?.nsfw || !!sticker.nsfw; const {shouldBlur, shouldBlock, canReveal, reveal} = useMatureMedia(isMature, message.channelId); const tooltipContent = () => (
{sticker.name} {guild && (
{guild.name}
)}
); const handleContextMenu = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const stickerForMenu = stickerRecord ?? { id: sticker.id, guildId: '', name: sticker.name, description: '', tags: [], url: stickerUrl, animated: sticker.animated, nsfw: Boolean(sticker.nsfw), user: undefined, }; ContextMenuCommands.openFromEvent(e, ({onClose}) => ( } data-flx="channel.message-attachments.handle-context-menu.message-context-menu" /> )); }; const handleRevealClick = useCallback( (e: React.MouseEvent) => { if (shouldBlur && canReveal) { e.preventDefault(); e.stopPropagation(); reveal(); } }, [shouldBlur, canReveal, reveal], ); if (shouldBlock) { return null; } const stickerImage = ( {stickerRecord?.description ); return ( ); }, ); export const MessageAttachments = observer(() => { const {channel, message, handleDelete, previewContext, onPopoutToggle, suppressMessageActions} = useMessageViewContext(); const isPreview = Boolean(previewContext); const reactionsIsPreview = isPreview || Boolean(suppressMessageActions); const reactions = useMessageReactionsSnapshot(message.id); const {shouldAnimate, interactionHandlers} = useStickerAnimation(); const spoileredUrlEmbeds = useMemo(() => { const embeddableCodeLinkContent = extractEmbeddableCodeLinkContent(message.content); return { invites: buildSpoileredCodeLinkMap(InviteUtils.findSpoileredInvites(embeddableCodeLinkContent), 'invite'), themes: buildSpoileredCodeLinkMap(ThemeUtils.findSpoileredThemes(embeddableCodeLinkContent), 'theme'), gifts: buildSpoileredCodeLinkMap(GiftCodeUtils.findSpoileredGifts(embeddableCodeLinkContent), 'gift'), }; }, [message.content]); return ( <> {message.messageSnapshots && message.messageSnapshots.length > 0 && ( )} {message.invites.map((code) => ( ))} {message.themes.map((themeId) => ( ))} {message.gifts.map((code) => ( ))} {message.stickers && message.stickers.length > 0 && (
{message.stickers.map((sticker: MessageStickerItem) => ( ))}
)} {(() => { const {enrichedAttachments, mediaAttachments, shouldUseMosaic} = getAttachmentRenderingState( message.attachments, ); return ( <> {shouldUseMosaic && ( )} {enrichedAttachments.map((attachment) => ( ))} ); })()} {UserSettings.getRenderEmbeds() && !message.suppressEmbeds && message.embeds.map((embed, index) => { const embedKey = `${embed.id}-${index}`; return ( ); })} {UserSettings.getRenderReactions() && reactions.length > 0 && ( )} ); });