mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
716 lines
25 KiB
TypeScript
716 lines
25 KiB
TypeScript
// 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<string, ReadonlyArray<string>>;
|
|
|
|
const EMPTY_SPOILER_KEYS: ReadonlyArray<string> = Object.freeze([]);
|
|
|
|
function getCodeLinkSpoilerSyncKeys(match: SpoileredCodeLinkMatch, keyPrefix: string): Array<string> {
|
|
const canonicalUrl = canonicalizeMediaUrl(match.matchedText) ?? canonicalizeMediaUrl(`https://${match.matchedText}`);
|
|
return canonicalUrl ? [`${keyPrefix}:${match.code}`, canonicalUrl] : [`${keyPrefix}:${match.code}`];
|
|
}
|
|
|
|
function buildSpoileredCodeLinkMap(
|
|
matches: ReadonlyArray<SpoileredCodeLinkMatch>,
|
|
keyPrefix: string,
|
|
): SpoileredCodeLinkMap {
|
|
const linksByCode = new Map<string, Array<string>>();
|
|
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<string>;
|
|
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 (
|
|
<SpoilerOverlay
|
|
hidden={hidden}
|
|
onReveal={reveal}
|
|
className={styles.urlEmbedSpoiler}
|
|
data-flx="channel.message-attachments.spoilered-url-embed"
|
|
>
|
|
{children}
|
|
</SpoilerOverlay>
|
|
);
|
|
});
|
|
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 (
|
|
<NotePencilIcon
|
|
className={styles.forwardedSourceIcon}
|
|
weight="fill"
|
|
size={iconSize}
|
|
data-flx="channel.message-attachments.render-channel-icon.forwarded-source-icon"
|
|
/>
|
|
);
|
|
}
|
|
if (sourceChannel.type === ChannelTypes.DM && sourceUser) {
|
|
return (
|
|
<div
|
|
className={styles.forwardedSourceAvatar}
|
|
data-flx="channel.message-attachments.render-channel-icon.forwarded-source-avatar"
|
|
>
|
|
<Avatar
|
|
user={sourceUser}
|
|
size={iconSize}
|
|
status={null}
|
|
data-flx="channel.message-attachments.render-channel-icon.avatar"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
if (sourceChannel.type === ChannelTypes.GROUP_DM) {
|
|
return (
|
|
<div
|
|
className={styles.forwardedSourceAvatar}
|
|
data-flx="channel.message-attachments.render-channel-icon.forwarded-source-avatar--2"
|
|
>
|
|
<GroupDMAvatar
|
|
channel={sourceChannel}
|
|
size={iconSize}
|
|
data-flx="channel.message-attachments.render-channel-icon.group-dm-avatar"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
if (sourceChannel.type === ChannelTypes.GUILD_VOICE) {
|
|
return (
|
|
<SpeakerHighIcon
|
|
className={styles.forwardedSourceIcon}
|
|
weight="fill"
|
|
size={iconSize}
|
|
data-flx="channel.message-attachments.render-channel-icon.forwarded-source-icon--2"
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<HashIcon
|
|
className={styles.forwardedSourceIcon}
|
|
weight="bold"
|
|
size={iconSize}
|
|
data-flx="channel.message-attachments.render-channel-icon.forwarded-source-icon--3"
|
|
/>
|
|
);
|
|
}, [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 (
|
|
<FocusRing data-flx="channel.message-attachments.forwarded-from-source.focus-ring">
|
|
<button
|
|
type="button"
|
|
onClick={handleJumpToOriginal}
|
|
className={styles.forwardedSourceButton}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-button.jump-to-original"
|
|
>
|
|
<span
|
|
className={styles.forwardedSourceLabel}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-label"
|
|
>
|
|
<Trans>Forwarded from</Trans>
|
|
</span>
|
|
<span
|
|
className={styles.forwardedSourceInfo}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-info"
|
|
>
|
|
{renderChannelIcon()}
|
|
<span
|
|
className={styles.forwardedSourceName}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-name"
|
|
>
|
|
{displayName}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</FocusRing>
|
|
);
|
|
}
|
|
if (sourceGuild) {
|
|
return (
|
|
<FocusRing data-flx="channel.message-attachments.forwarded-from-source.focus-ring--2">
|
|
<button
|
|
type="button"
|
|
onClick={handleJumpToOriginal}
|
|
className={styles.forwardedSourceButton}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-button.jump-to-original--2"
|
|
>
|
|
<span
|
|
className={styles.forwardedSourceLabel}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-label--2"
|
|
>
|
|
<Trans>Forwarded from</Trans>
|
|
</span>
|
|
<span
|
|
className={styles.forwardedSourceInfo}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-info--2"
|
|
>
|
|
<GuildIcon
|
|
id={sourceGuild.id}
|
|
name={sourceGuild.name}
|
|
icon={sourceGuild.icon}
|
|
className={styles.forwardedSourceGuildIcon}
|
|
sizePx={16}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-guild-icon"
|
|
/>
|
|
<span
|
|
className={styles.forwardedSourceName}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-name--2"
|
|
>
|
|
{sourceGuild.name}
|
|
</span>
|
|
<CaretRightIcon
|
|
className={styles.forwardedSourceChevron}
|
|
weight="bold"
|
|
size={12}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-chevron"
|
|
/>
|
|
{renderChannelIcon()}
|
|
<span
|
|
className={styles.forwardedSourceName}
|
|
data-flx="channel.message-attachments.forwarded-from-source.forwarded-source-name--3"
|
|
>
|
|
{displayName}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</FocusRing>
|
|
);
|
|
}
|
|
return null;
|
|
});
|
|
|
|
interface ForwardedMessageContentProps {
|
|
message: Message;
|
|
snapshot: MessageSnapshot;
|
|
shouldAnimate: boolean;
|
|
interactionHandlers: Record<string, unknown>;
|
|
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 (
|
|
<div
|
|
className={styles.forwardedContainer}
|
|
data-message-copy-block={copyText ? 'true' : undefined}
|
|
data-message-copy-text={copyText || undefined}
|
|
data-flx="channel.message-attachments.forwarded-message-content.forwarded-container"
|
|
>
|
|
<div
|
|
className={styles.forwardedBar}
|
|
data-flx="channel.message-attachments.forwarded-message-content.forwarded-bar"
|
|
/>
|
|
<div
|
|
className={styles.forwardedContent}
|
|
data-flx="channel.message-attachments.forwarded-message-content.forwarded-content"
|
|
>
|
|
<div
|
|
className={styles.forwardedHeader}
|
|
data-flx="channel.message-attachments.forwarded-message-content.forwarded-header"
|
|
>
|
|
<ArrowBendUpRightIcon
|
|
className={styles.forwardedIcon}
|
|
weight="bold"
|
|
data-flx="channel.message-attachments.forwarded-message-content.forwarded-icon"
|
|
/>
|
|
<span
|
|
className={styles.forwardedLabel}
|
|
data-flx="channel.message-attachments.forwarded-message-content.forwarded-label"
|
|
>
|
|
<Trans>Forwarded</Trans>
|
|
</span>
|
|
</div>
|
|
{snapshot.content && (
|
|
<div
|
|
className={clsx(markupStyles.markup)}
|
|
data-search-highlight-scope="message"
|
|
data-flx="channel.message-attachments.forwarded-message-content.div"
|
|
>
|
|
<SafeMarkdown
|
|
content={snapshot.content}
|
|
options={{
|
|
context: MarkdownContext.STANDARD_WITH_JUMBO,
|
|
messageId: message.id,
|
|
channelId: message.channelId,
|
|
mentionChannels: snapshot.mention_channels,
|
|
}}
|
|
data-flx="channel.message-attachments.forwarded-message-content.safe-markdown"
|
|
/>
|
|
{snapshotEditedTimestamp && (
|
|
<TimestampWithTooltip
|
|
date={snapshotEditedTimestamp}
|
|
className={messageStyles.editedTimestamp}
|
|
data-flx="channel.message-attachments.forwarded-message-content.timestamp-with-tooltip"
|
|
>
|
|
<span
|
|
className={messageStyles.editedLabel}
|
|
data-flx="channel.message-attachments.forwarded-message-content.span"
|
|
>
|
|
{' '}
|
|
<Trans>(edited)</Trans>
|
|
</span>
|
|
</TimestampWithTooltip>
|
|
)}
|
|
</div>
|
|
)}
|
|
{snapshot.attachments && snapshot.attachments.length > 0 && (
|
|
<div
|
|
className={styles.attachmentsContainer}
|
|
data-flx="channel.message-attachments.forwarded-message-content.attachments-container"
|
|
>
|
|
{(() => {
|
|
const {enrichedAttachments, mediaAttachments, shouldUseMosaic} = getAttachmentRenderingState(
|
|
snapshot.attachments,
|
|
);
|
|
return (
|
|
<>
|
|
{shouldUseMosaic && (
|
|
<AttachmentMosaic
|
|
attachments={mediaAttachments}
|
|
message={message}
|
|
snapshotIndex={snapshotIndex}
|
|
onDelete={onDelete}
|
|
data-flx="channel.message-attachments.forwarded-message-content.attachment-mosaic"
|
|
/>
|
|
)}
|
|
{enrichedAttachments.map((attachment: MessageAttachment) => (
|
|
<Attachment
|
|
key={attachment.id}
|
|
attachment={attachment}
|
|
snapshotIndex={snapshotIndex}
|
|
message={message}
|
|
renderInMosaic={shouldUseMosaic}
|
|
onDelete={onDelete}
|
|
data-flx="channel.message-attachments.forwarded-message-content.attachment"
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
})()}
|
|
</div>
|
|
)}
|
|
{snapshot.embeds && snapshot.embeds.length > 0 && UserSettings.getRenderEmbeds() && (
|
|
<div
|
|
className={styles.attachmentsContainer}
|
|
data-flx="channel.message-attachments.forwarded-message-content.attachments-container--2"
|
|
>
|
|
{snapshot.embeds.map((embed: MessageEmbed, index: number) => {
|
|
const embedKey = `${embed.id}-${index}`;
|
|
return (
|
|
<Embed
|
|
embed={embed}
|
|
key={embedKey}
|
|
message={message}
|
|
embedIndex={index}
|
|
contextualEmbeds={snapshot.embeds}
|
|
onDelete={onDelete}
|
|
isPreview={true}
|
|
data-flx="channel.message-attachments.forwarded-message-content.embed"
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
{snapshot.stickers && snapshot.stickers.length > 0 && (
|
|
<div
|
|
className={styles.stickersContainer}
|
|
data-flx="channel.message-attachments.forwarded-message-content.stickers-container"
|
|
>
|
|
{snapshot.stickers.map((sticker: MessageStickerItem) => (
|
|
<StickerItem
|
|
key={sticker.id}
|
|
sticker={sticker}
|
|
message={message}
|
|
shouldAnimate={shouldAnimate}
|
|
interactionHandlers={interactionHandlers}
|
|
handleDelete={onDelete}
|
|
data-flx="channel.message-attachments.forwarded-message-content.sticker-item"
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
<ForwardedFromSource
|
|
message={message}
|
|
data-flx="channel.message-attachments.forwarded-message-content.forwarded-from-source"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
interface StickerItemProps {
|
|
sticker: MessageStickerItem;
|
|
message: Message;
|
|
shouldAnimate: boolean;
|
|
interactionHandlers: Record<string, unknown>;
|
|
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 = () => (
|
|
<div className={styles.stickerTooltip} data-flx="channel.message-attachments.tooltip-content.sticker-tooltip">
|
|
<span className={styles.stickerName} data-flx="channel.message-attachments.tooltip-content.sticker-name">
|
|
{sticker.name}
|
|
</span>
|
|
{guild && (
|
|
<div
|
|
className={styles.stickerGuildInfo}
|
|
data-flx="channel.message-attachments.tooltip-content.sticker-guild-info"
|
|
>
|
|
<GuildIcon
|
|
id={guild.id}
|
|
name={guild.name}
|
|
icon={guild.icon}
|
|
className={styles.stickerGuildIcon}
|
|
sizePx={16}
|
|
data-flx="channel.message-attachments.tooltip-content.sticker-guild-icon"
|
|
/>
|
|
<span
|
|
className={styles.stickerGuildName}
|
|
data-flx="channel.message-attachments.tooltip-content.sticker-guild-name"
|
|
>
|
|
{guild.name}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
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}) => (
|
|
<MessageContextMenu
|
|
message={message}
|
|
sourceChannel={sourceChannel}
|
|
onClose={onClose}
|
|
onDelete={handleDelete!}
|
|
inlineStickerOrEmojiItems={
|
|
<StickerInlineMenuItems
|
|
sticker={stickerForMenu}
|
|
onClose={onClose}
|
|
data-flx="channel.message-attachments.handle-context-menu.sticker-inline-menu-items"
|
|
/>
|
|
}
|
|
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 = (
|
|
<img
|
|
src={stickerUrl}
|
|
alt={stickerRecord?.description || sticker.name}
|
|
className={clsx(styles.stickerImage, shouldBlur && matureStyles.matureStickerBlurred)}
|
|
width="160"
|
|
height="160"
|
|
data-flx="channel.message-attachments.sticker-item.sticker-image"
|
|
/>
|
|
);
|
|
return (
|
|
<Tooltip key={sticker.id} text={tooltipContent} data-flx="channel.message-attachments.sticker-item.tooltip">
|
|
<FocusRing data-flx="channel.message-attachments.sticker-item.focus-ring">
|
|
<button
|
|
type="button"
|
|
aria-label={stickerRecord?.description || sticker.name}
|
|
className={styles.stickerWrapper}
|
|
data-message-sticker="true"
|
|
onContextMenu={handleContextMenu}
|
|
onClick={handleRevealClick}
|
|
data-flx="channel.message-attachments.sticker-item.sticker-wrapper.reveal-click"
|
|
{...interactionHandlers}
|
|
>
|
|
{stickerImage}
|
|
</button>
|
|
</FocusRing>
|
|
</Tooltip>
|
|
);
|
|
},
|
|
);
|
|
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 && (
|
|
<ForwardedMessageContent
|
|
message={message}
|
|
snapshot={message.messageSnapshots[0]}
|
|
shouldAnimate={shouldAnimate}
|
|
interactionHandlers={interactionHandlers}
|
|
onDelete={handleDelete}
|
|
data-flx="channel.message-attachments.forwarded-message-content"
|
|
/>
|
|
)}
|
|
{message.invites.map((code) => (
|
|
<SpoileredUrlEmbed
|
|
key={code}
|
|
channelId={message.channelId}
|
|
spoilerKeys={spoileredUrlEmbeds.invites.get(code)}
|
|
data-flx="channel.message-attachments.spoilered-url-embed"
|
|
>
|
|
<FocusRing data-flx="channel.message-attachments.focus-ring">
|
|
<InviteEmbed
|
|
code={code}
|
|
message={message}
|
|
sourceChannel={channel}
|
|
onDelete={handleDelete}
|
|
data-flx="channel.message-attachments.invite-embed"
|
|
/>
|
|
</FocusRing>
|
|
</SpoileredUrlEmbed>
|
|
))}
|
|
{message.themes.map((themeId) => (
|
|
<SpoileredUrlEmbed
|
|
key={themeId}
|
|
channelId={message.channelId}
|
|
spoilerKeys={spoileredUrlEmbeds.themes.get(themeId)}
|
|
data-flx="channel.message-attachments.spoilered-url-embed--2"
|
|
>
|
|
<FocusRing data-flx="channel.message-attachments.focus-ring--2">
|
|
<ThemeEmbed themeId={themeId} data-flx="channel.message-attachments.theme-embed" />
|
|
</FocusRing>
|
|
</SpoileredUrlEmbed>
|
|
))}
|
|
{message.gifts.map((code) => (
|
|
<SpoileredUrlEmbed
|
|
key={code}
|
|
channelId={message.channelId}
|
|
spoilerKeys={spoileredUrlEmbeds.gifts.get(code)}
|
|
data-flx="channel.message-attachments.spoilered-url-embed--3"
|
|
>
|
|
<FocusRing data-flx="channel.message-attachments.focus-ring--3">
|
|
<GiftEmbed code={code} data-flx="channel.message-attachments.gift-embed" />
|
|
</FocusRing>
|
|
</SpoileredUrlEmbed>
|
|
))}
|
|
{message.stickers && message.stickers.length > 0 && (
|
|
<div className={styles.stickersContainer} data-flx="channel.message-attachments.stickers-container">
|
|
{message.stickers.map((sticker: MessageStickerItem) => (
|
|
<StickerItem
|
|
key={sticker.id}
|
|
sticker={sticker}
|
|
message={message}
|
|
shouldAnimate={shouldAnimate}
|
|
interactionHandlers={interactionHandlers}
|
|
sourceChannel={channel}
|
|
handleDelete={handleDelete}
|
|
data-flx="channel.message-attachments.sticker-item"
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
{(() => {
|
|
const {enrichedAttachments, mediaAttachments, shouldUseMosaic} = getAttachmentRenderingState(
|
|
message.attachments,
|
|
);
|
|
return (
|
|
<>
|
|
{shouldUseMosaic && (
|
|
<AttachmentMosaic
|
|
attachments={mediaAttachments}
|
|
message={message}
|
|
isPreview={isPreview}
|
|
onDelete={handleDelete}
|
|
data-flx="channel.message-attachments.attachment-mosaic"
|
|
/>
|
|
)}
|
|
{enrichedAttachments.map((attachment) => (
|
|
<Attachment
|
|
key={attachment.id}
|
|
attachment={attachment}
|
|
isPreview={isPreview}
|
|
message={message}
|
|
renderInMosaic={shouldUseMosaic}
|
|
onDelete={handleDelete}
|
|
data-flx="channel.message-attachments.attachment"
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
})()}
|
|
{UserSettings.getRenderEmbeds() &&
|
|
!message.suppressEmbeds &&
|
|
message.embeds.map((embed, index) => {
|
|
const embedKey = `${embed.id}-${index}`;
|
|
return (
|
|
<Embed
|
|
embed={embed}
|
|
key={embedKey}
|
|
message={message}
|
|
embedIndex={index}
|
|
onDelete={handleDelete}
|
|
isPreview={isPreview}
|
|
data-flx="channel.message-attachments.embed"
|
|
/>
|
|
);
|
|
})}
|
|
{UserSettings.getRenderReactions() && reactions.length > 0 && (
|
|
<MessageReactions
|
|
message={message}
|
|
isPreview={reactionsIsPreview}
|
|
onPopoutToggle={onPopoutToggle}
|
|
data-flx="channel.message-attachments.message-reactions"
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
});
|