/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {Trans} 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 React from 'react'; import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators'; import {ChannelTypes, StickerFormatTypes} from '~/Constants'; import {Attachment} from '~/components/channel/embeds/attachments/Attachment'; import {AttachmentMosaic} from '~/components/channel/embeds/attachments/AttachmentMosaic'; import {Embed} from '~/components/channel/embeds/Embed'; import {GiftEmbed} from '~/components/channel/GiftEmbed'; import {InviteEmbed} from '~/components/channel/InviteEmbed'; import {MessageReactions} from '~/components/channel/MessageReactions'; import {getAttachmentRenderingState} from '~/components/channel/messageAttachmentStateUtils'; import {ThemeEmbed} from '~/components/channel/ThemeEmbed'; import {GroupDMAvatar} from '~/components/common/GroupDMAvatar'; import {GuildIcon} from '~/components/popouts/GuildIcon'; import {Avatar} from '~/components/uikit/Avatar'; import {MediaContextMenu} from '~/components/uikit/ContextMenu/MediaContextMenu'; import FocusRing from '~/components/uikit/FocusRing/FocusRing'; import {Tooltip} from '~/components/uikit/Tooltip/Tooltip'; import {SafeMarkdown} from '~/lib/markdown'; import {MarkdownContext} from '~/lib/markdown/renderers'; import type { MessageAttachment, MessageEmbed, MessageRecord, MessageSnapshot, MessageStickerItem, } from '~/records/MessageRecord'; import GuildStore from '~/stores/GuildStore'; import StickerStore from '~/stores/StickerStore'; import UserSettingsStore from '~/stores/UserSettingsStore'; import markupStyles from '~/styles/Markup.module.css'; import * as AvatarUtils from '~/utils/AvatarUtils'; import {useForwardedMessageContext} from '~/utils/forwardedMessageUtils'; import {goToMessage} from '~/utils/MessageNavigator'; import styles from './MessageAttachments.module.css'; import {useMessageViewContext} from './MessageViewContext'; const ForwardedFromSource = observer(({message}: {message: MessageRecord}) => { const {sourceChannel, sourceGuild, sourceUser, hasAccessToSource, displayName} = useForwardedMessageContext(message); const handleJumpToOriginal = React.useCallback(() => { if (message.messageReference && sourceChannel) { goToMessage(message.messageReference.channel_id, message.messageReference.message_id); } }, [message.messageReference, sourceChannel]); if (!hasAccessToSource || !sourceChannel || !displayName || !message.messageReference) { return null; } const renderChannelIcon = () => { 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 ; }; if ( sourceChannel.type === ChannelTypes.DM || sourceChannel.type === ChannelTypes.GROUP_DM || sourceChannel.type === ChannelTypes.DM_PERSONAL_NOTES ) { return ( ); } if (sourceGuild) { return ( ); } return null; }); const ForwardedMessageContent = observer(({message, snapshot}: {message: MessageRecord; snapshot: MessageSnapshot}) => { return (
Forwarded
{snapshot.content && (
)} {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 && UserSettingsStore.getRenderEmbeds() && (
{snapshot.embeds.map((embed: MessageEmbed, index: number) => ( {}} /> ))}
)}
); }); export const MessageAttachments = observer(() => { const {message, handleDelete, previewContext, onPopoutToggle} = useMessageViewContext(); const isPreview = Boolean(previewContext); 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 stickerUrl = AvatarUtils.getStickerURL({ id: sticker.id, animated: sticker.format_type === StickerFormatTypes.GIF, size: 320, }); const stickerRecord = StickerStore.getStickerById(sticker.id); const guild = stickerRecord?.guildId ? GuildStore.getGuild(stickerRecord.guildId) : null; const tooltipContent = () => (
{sticker.name} {guild && (
{guild.name}
)}
); const handleContextMenu = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); ContextMenuActionCreators.openFromEvent(e, ({onClose}) => ( )); }; return (
{stickerRecord?.description
); })}
)} {(() => { const {enrichedAttachments, mediaAttachments} = getAttachmentRenderingState(message.attachments); const inlineMedia = UserSettingsStore.getInlineAttachmentMedia(); const shouldWrapInMosaic = inlineMedia && mediaAttachments.length > 0; return ( <> {shouldWrapInMosaic && } {enrichedAttachments.map((attachment) => ( ))} ); })()} {UserSettingsStore.getRenderEmbeds() && !message.suppressEmbeds && message.embeds.map((embed, index) => ( ))} {UserSettingsStore.getRenderReactions() && message.reactions.length > 0 && ( )} ); });