fix(embed): hide remove buttons when unapplicable (#66)

This commit is contained in:
Hampus 2026-01-06 19:33:11 +01:00 committed by GitHub
parent c50a74db7b
commit 056d578965
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 579 additions and 488 deletions

View File

@ -145,6 +145,7 @@ const ForwardedFromSource = observer(({message}: {message: MessageRecord}) => {
}); });
const ForwardedMessageContent = observer(({message, snapshot}: {message: MessageRecord; snapshot: MessageSnapshot}) => { const ForwardedMessageContent = observer(({message, snapshot}: {message: MessageRecord; snapshot: MessageSnapshot}) => {
const snapshotIsPreview = true;
return ( return (
<div className={styles.forwardedContainer}> <div className={styles.forwardedContainer}>
<div className={styles.forwardedBar} /> <div className={styles.forwardedBar} />
@ -177,12 +178,14 @@ const ForwardedMessageContent = observer(({message, snapshot}: {message: Message
); );
return ( return (
<> <>
{shouldUseMosaic && <AttachmentMosaic attachments={mediaAttachments} message={message} />} {shouldUseMosaic && (
<AttachmentMosaic attachments={mediaAttachments} message={message} isPreview={snapshotIsPreview} />
)}
{enrichedAttachments.map((attachment: MessageAttachment) => ( {enrichedAttachments.map((attachment: MessageAttachment) => (
<Attachment <Attachment
key={attachment.id} key={attachment.id}
attachment={attachment} attachment={attachment}
isPreview={false} isPreview={snapshotIsPreview}
message={message} message={message}
renderInMosaic={shouldUseMosaic} renderInMosaic={shouldUseMosaic}
/> />
@ -205,6 +208,7 @@ const ForwardedMessageContent = observer(({message, snapshot}: {message: Message
embedIndex={index} embedIndex={index}
contextualEmbeds={snapshot.embeds} contextualEmbeds={snapshot.embeds}
onDelete={() => {}} onDelete={() => {}}
isPreview={snapshotIsPreview}
/> />
); );
})} })}
@ -315,7 +319,9 @@ export const MessageAttachments = observer(() => {
const shouldWrapInMosaic = inlineMedia && mediaAttachments.length > 0; const shouldWrapInMosaic = inlineMedia && mediaAttachments.length > 0;
return ( return (
<> <>
{shouldWrapInMosaic && <AttachmentMosaic attachments={mediaAttachments} message={message} />} {shouldWrapInMosaic && (
<AttachmentMosaic attachments={mediaAttachments} message={message} isPreview={isPreview} />
)}
{enrichedAttachments.map((attachment) => ( {enrichedAttachments.map((attachment) => (
<Attachment <Attachment
key={attachment.id} key={attachment.id}
@ -334,7 +340,14 @@ export const MessageAttachments = observer(() => {
message.embeds.map((embed, index) => { message.embeds.map((embed, index) => {
const embedKey = `${embed.id}-${index}`; const embedKey = `${embed.id}-${index}`;
return ( return (
<Embed embed={embed} key={embedKey} message={message} embedIndex={index} onDelete={handleDelete} /> <Embed
embed={embed}
key={embedKey}
message={message}
embedIndex={index}
onDelete={handleDelete}
isPreview={isPreview}
/>
); );
})} })}

View File

@ -74,6 +74,7 @@ interface EmbedProps {
embedIndex?: number; embedIndex?: number;
onDelete?: (bypassConfirm?: boolean) => void; onDelete?: (bypassConfirm?: boolean) => void;
contextualEmbeds?: ReadonlyArray<MessageEmbed>; contextualEmbeds?: ReadonlyArray<MessageEmbed>;
isPreview?: boolean;
} }
interface LinkComponentProps { interface LinkComponentProps {
@ -432,7 +433,8 @@ const EmbedMediaRenderer: FC<{
message: MessageRecord; message: MessageRecord;
embedIndex?: number; embedIndex?: number;
onDelete?: (bypassConfirm?: boolean) => void; onDelete?: (bypassConfirm?: boolean) => void;
}> = observer(({embed, message, embedIndex, onDelete}) => { isPreview?: boolean;
}> = observer(({embed, message, embedIndex, onDelete, isPreview}) => {
const {video, image, thumbnail} = embed; const {video, image, thumbnail} = embed;
if (!isValidMedia(video) && !isValidMedia(image) && !isValidMedia(thumbnail)) { if (!isValidMedia(video) && !isValidMedia(image) && !isValidMedia(thumbnail)) {
@ -466,6 +468,7 @@ const EmbedMediaRenderer: FC<{
contentHash={video.content_hash} contentHash={video.content_hash}
embedIndex={embedIndex} embedIndex={embedIndex}
onDelete={onDelete} onDelete={onDelete}
isPreview={isPreview}
/> />
</FocusRing> </FocusRing>
); );
@ -491,6 +494,7 @@ const EmbedMediaRenderer: FC<{
contentHash={image.content_hash} contentHash={image.content_hash}
embedIndex={embedIndex} embedIndex={embedIndex}
onDelete={onDelete} onDelete={onDelete}
isPreview={isPreview}
/> />
</FocusRing> </FocusRing>
); );
@ -514,6 +518,7 @@ const EmbedMediaRenderer: FC<{
contentHash={image.content_hash} contentHash={image.content_hash}
embedIndex={embedIndex} embedIndex={embedIndex}
onDelete={onDelete} onDelete={onDelete}
isPreview={isPreview}
/> />
</FocusRing> </FocusRing>
); );
@ -570,7 +575,7 @@ const EmbedMediaRenderer: FC<{
return null; return null;
}); });
const RichEmbed: FC<EmbedProps> = observer(({embed, message, embedIndex, contextualEmbeds, onDelete}) => { const RichEmbed: FC<EmbedProps> = observer(({embed, message, embedIndex, contextualEmbeds, onDelete, isPreview}) => {
const embedList = contextualEmbeds ?? message.embeds; const embedList = contextualEmbeds ?? message.embeds;
const hasVideo = isValidMedia(embed.video); const hasVideo = isValidMedia(embed.video);
const hasImage = isValidMedia(embed.image); const hasImage = isValidMedia(embed.image);
@ -640,9 +645,20 @@ const RichEmbed: FC<EmbedProps> = observer(({embed, message, embedIndex, context
{!shouldRenderInlineThumbnail && hasAnyMedia && ( {!shouldRenderInlineThumbnail && hasAnyMedia && (
<div className={clsx(styles.embedMedia)}> <div className={clsx(styles.embedMedia)}>
{showGallery && galleryAttachments ? ( {showGallery && galleryAttachments ? (
<AttachmentMosaic attachments={galleryAttachments} message={message} hideExpiryFootnote={true} /> <AttachmentMosaic
attachments={galleryAttachments}
message={message}
hideExpiryFootnote={true}
isPreview={isPreview}
/>
) : ( ) : (
<EmbedMediaRenderer embed={embed} message={message} embedIndex={embedIndex} onDelete={onDelete} /> <EmbedMediaRenderer
embed={embed}
message={message}
embedIndex={embedIndex}
onDelete={onDelete}
isPreview={isPreview}
/>
)} )}
</div> </div>
)} )}
@ -683,6 +699,7 @@ const RichEmbed: FC<EmbedProps> = observer(({embed, message, embedIndex, context
contentHash={embed.thumbnail.content_hash} contentHash={embed.thumbnail.content_hash}
embedIndex={embedIndex} embedIndex={embedIndex}
onDelete={onDelete} onDelete={onDelete}
isPreview={isPreview}
/> />
</FocusRing> </FocusRing>
</div> </div>
@ -693,7 +710,7 @@ const RichEmbed: FC<EmbedProps> = observer(({embed, message, embedIndex, context
); );
}); });
export const Embed: FC<EmbedProps> = observer(({embed, message, embedIndex, contextualEmbeds, onDelete}) => { export const Embed: FC<EmbedProps> = observer(({embed, message, embedIndex, contextualEmbeds, onDelete, isPreview}) => {
const {t} = useLingui(); const {t} = useLingui();
const {enabled: isMobile} = MobileLayoutStore; const {enabled: isMobile} = MobileLayoutStore;
const channel = ChannelStore.getChannel(message.channelId); const channel = ChannelStore.getChannel(message.channelId);
@ -725,7 +742,8 @@ export const Embed: FC<EmbedProps> = observer(({embed, message, embedIndex, cont
ModalActionCreators.push(modal(() => <SuppressEmbedsConfirmModal message={message} />)); ModalActionCreators.push(modal(() => <SuppressEmbedsConfirmModal message={message} />));
}, [message]); }, [message]);
const showSuppressButton = !isMobile && canSuppressEmbeds() && AccessibilityStore.showSuppressEmbedsButton; const showSuppressButton =
!isMobile && canSuppressEmbeds() && AccessibilityStore.showSuppressEmbedsButton && !isPreview;
const spoileredUrls = useMemo(() => extractSpoileredUrls(message.content), [message.content]); const spoileredUrls = useMemo(() => extractSpoileredUrls(message.content), [message.content]);
const {isSpoilerEmbed, matchingSpoilerUrls} = useMemo(() => { const {isSpoilerEmbed, matchingSpoilerUrls} = useMemo(() => {
@ -820,6 +838,7 @@ export const Embed: FC<EmbedProps> = observer(({embed, message, embedIndex, cont
contentHash={embed.audio.content_hash} contentHash={embed.audio.content_hash}
embedIndex={embedIndex} embedIndex={embedIndex}
onDelete={onDelete} onDelete={onDelete}
isPreview={isPreview}
/> />
</FocusRing> </FocusRing>
</div> </div>
@ -1072,6 +1091,7 @@ export const Embed: FC<EmbedProps> = observer(({embed, message, embedIndex, cont
embedIndex={embedIndex} embedIndex={embedIndex}
contextualEmbeds={contextualEmbeds} contextualEmbeds={contextualEmbeds}
onDelete={onDelete} onDelete={onDelete}
isPreview={isPreview}
/> />
</div>, </div>,
); );

View File

@ -67,7 +67,8 @@ const isUploading = (flags: number): boolean => (flags & 0x1000) !== 0;
const hasValidDimensions = (attachment: MessageAttachment): boolean => const hasValidDimensions = (attachment: MessageAttachment): boolean =>
typeof attachment.width === 'number' && typeof attachment.height === 'number'; typeof attachment.width === 'number' && typeof attachment.height === 'number';
const AnimatedAttachment: FC<AttachmentMediaProps & {message?: MessageRecord}> = observer(({attachment, message}) => { const AnimatedAttachment: FC<AttachmentMediaProps & {message?: MessageRecord; isPreview?: boolean}> = observer(
({attachment, message, isPreview}) => {
const embedUrl = attachment.url ?? ''; const embedUrl = attachment.url ?? '';
const proxyUrl = attachment.proxy_url ?? embedUrl; const proxyUrl = attachment.proxy_url ?? embedUrl;
const animatedProxyURL = buildMediaProxyURL(proxyUrl, { const animatedProxyURL = buildMediaProxyURL(proxyUrl, {
@ -89,13 +90,15 @@ const AnimatedAttachment: FC<AttachmentMediaProps & {message?: MessageRecord}> =
attachmentId={attachment.id} attachmentId={attachment.id}
message={message} message={message}
contentHash={attachment.content_hash} contentHash={attachment.content_hash}
isPreview={isPreview}
/> />
</FocusRing> </FocusRing>
); );
}); },
);
const VideoAttachment: FC<AttachmentMediaProps & {message?: MessageRecord}> = observer( const VideoAttachment: FC<AttachmentMediaProps & {message?: MessageRecord; isPreview?: boolean}> = observer(
({attachment, message, mediaAttachments = []}) => { ({attachment, message, mediaAttachments = [], isPreview}) => {
const embedUrl = attachment.url ?? ''; const embedUrl = attachment.url ?? '';
const proxyUrl = attachment.proxy_url ?? embedUrl; const proxyUrl = attachment.proxy_url ?? embedUrl;
const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0; const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0;
@ -132,6 +135,7 @@ const VideoAttachment: FC<AttachmentMediaProps & {message?: MessageRecord}> = ob
message={message} message={message}
contentHash={attachment.content_hash} contentHash={attachment.content_hash}
mediaAttachments={mediaAttachments} mediaAttachments={mediaAttachments}
isPreview={isPreview}
/> />
</div> </div>
</FocusRing> </FocusRing>
@ -139,7 +143,8 @@ const VideoAttachment: FC<AttachmentMediaProps & {message?: MessageRecord}> = ob
}, },
); );
const AudioAttachment: FC<AttachmentMediaProps & {message?: MessageRecord}> = observer(({attachment, message}) => ( const AudioAttachment: FC<AttachmentMediaProps & {message?: MessageRecord; isPreview?: boolean}> = observer(
({attachment, message, isPreview}) => (
<FocusRing within ringClassName={messageStyles.mediaFocusRing}> <FocusRing within ringClassName={messageStyles.mediaFocusRing}>
<div className={styles.attachmentWrapper}> <div className={styles.attachmentWrapper}>
<EmbedAudio <EmbedAudio
@ -152,16 +157,18 @@ const AudioAttachment: FC<AttachmentMediaProps & {message?: MessageRecord}> = ob
attachmentId={attachment.id} attachmentId={attachment.id}
message={message} message={message}
contentHash={attachment.content_hash} contentHash={attachment.content_hash}
isPreview={isPreview}
/> />
</div> </div>
</FocusRing> </FocusRing>
)); ),
);
const AttachmentMedia: FC<AttachmentMediaProps & {message?: MessageRecord}> = observer( const AttachmentMedia: FC<AttachmentMediaProps & {message?: MessageRecord; isPreview?: boolean}> = observer(
({attachment, message, mediaAttachments = []}) => { ({attachment, message, mediaAttachments = [], isPreview}) => {
const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0; const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0;
if (isAnimated(attachment.flags) || isGifType(attachment.content_type)) { if (isAnimated(attachment.flags) || isGifType(attachment.content_type)) {
return <AnimatedAttachment attachment={attachment} message={message} />; return <AnimatedAttachment attachment={attachment} message={message} isPreview={isPreview} />;
} }
const attachmentDimensions = getAttachmentMediaDimensions(message); const attachmentDimensions = getAttachmentMediaDimensions(message);
@ -208,6 +215,7 @@ const AttachmentMedia: FC<AttachmentMediaProps & {message?: MessageRecord}> = ob
message={message} message={message}
contentHash={attachment.content_hash} contentHash={attachment.content_hash}
mediaAttachments={mediaAttachments} mediaAttachments={mediaAttachments}
isPreview={isPreview}
/> />
</div> </div>
</FocusRing> </FocusRing>
@ -283,7 +291,7 @@ export const Attachment: FC<AttachmentProps> = observer(({attachment, isPreview,
wrapSpoiler( wrapSpoiler(
<div className={effectiveExpired ? styles.expiredContent : undefined}> <div className={effectiveExpired ? styles.expiredContent : undefined}>
{effectiveExpired && <div className={styles.expiredOverlay}>{t`This attachment has expired`}</div>} {effectiveExpired && <div className={styles.expiredOverlay}>{t`This attachment has expired`}</div>}
<AudioAttachment attachment={enrichedAttachment} message={message} /> <AudioAttachment attachment={enrichedAttachment} message={message} isPreview={isPreview} />
</div>, </div>,
), ),
); );
@ -304,7 +312,7 @@ export const Attachment: FC<AttachmentProps> = observer(({attachment, isPreview,
wrapSpoiler( wrapSpoiler(
<div className={effectiveExpired ? styles.expiredContent : undefined}> <div className={effectiveExpired ? styles.expiredContent : undefined}>
{effectiveExpired && <div className={styles.expiredOverlay}>{t`This attachment has expired`}</div>} {effectiveExpired && <div className={styles.expiredOverlay}>{t`This attachment has expired`}</div>}
<AttachmentMedia attachment={enrichedAttachment} message={message} /> <AttachmentMedia attachment={enrichedAttachment} message={message} isPreview={isPreview} />
</div>, </div>,
), ),
); );
@ -315,7 +323,7 @@ export const Attachment: FC<AttachmentProps> = observer(({attachment, isPreview,
wrapSpoiler( wrapSpoiler(
<div className={effectiveExpired ? styles.expiredContent : undefined}> <div className={effectiveExpired ? styles.expiredContent : undefined}>
{effectiveExpired && <div className={styles.expiredOverlay}>{t`This attachment has expired`}</div>} {effectiveExpired && <div className={styles.expiredOverlay}>{t`This attachment has expired`}</div>}
<VideoAttachment attachment={enrichedAttachment} message={message} /> <VideoAttachment attachment={enrichedAttachment} message={message} isPreview={isPreview} />
</div>, </div>,
), ),
); );

View File

@ -37,6 +37,7 @@ import {clsx} from 'clsx';
import {observer} from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators'; import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators';
import {splitFilename} from '~/components/channel/embeds/EmbedUtils'; import {splitFilename} from '~/components/channel/embeds/EmbedUtils';
import {useMaybeMessageViewContext} from '~/components/channel/MessageViewContext';
import {canDeleteAttachmentUtil} from '~/components/channel/messageActionUtils'; import {canDeleteAttachmentUtil} from '~/components/channel/messageActionUtils';
import {MediaContextMenu} from '~/components/uikit/ContextMenu/MediaContextMenu'; import {MediaContextMenu} from '~/components/uikit/ContextMenu/MediaContextMenu';
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip'; import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
@ -54,7 +55,7 @@ interface AttachmentFileProps {
message?: MessageRecord; message?: MessageRecord;
} }
export const AttachmentFile = observer(({attachment, message}: AttachmentFileProps) => { export const AttachmentFile = observer(({attachment, message, isPreview}: AttachmentFileProps) => {
const {t} = useLingui(); const {t} = useLingui();
const {enabled: isMobile} = MobileLayoutStore; const {enabled: isMobile} = MobileLayoutStore;
const isExpired = Boolean(attachment.expired); const isExpired = Boolean(attachment.expired);
@ -131,9 +132,11 @@ export const AttachmentFile = observer(({attachment, message}: AttachmentFilePro
const handleDelete = useDeleteAttachment(message, attachment.id); const handleDelete = useDeleteAttachment(message, attachment.id);
const canDelete = canDeleteAttachmentUtil(message) && !isMobile; const canDelete = canDeleteAttachmentUtil(message) && !isMobile;
const showDeleteButton = canDelete && !isPreview;
const messageViewContext = useMaybeMessageViewContext();
const handleContextMenu = (e: React.MouseEvent) => { const handleContextMenu = (e: React.MouseEvent) => {
if (!message) return; if (!message || isPreview) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -148,7 +151,7 @@ export const AttachmentFile = observer(({attachment, message}: AttachmentFilePro
defaultName={attachment.filename} defaultName={attachment.filename}
defaultAltText={attachment.filename} defaultAltText={attachment.filename}
onClose={onClose} onClose={onClose}
onDelete={() => {}} onDelete={isPreview ? () => {} : (messageViewContext?.handleDelete ?? (() => {}))}
/> />
)); ));
}; };
@ -156,7 +159,7 @@ export const AttachmentFile = observer(({attachment, message}: AttachmentFilePro
return ( return (
// biome-ignore lint/a11y/noStaticElementInteractions: context menu on container is intentional // biome-ignore lint/a11y/noStaticElementInteractions: context menu on container is intentional
<div style={containerStyles} className={attachmentFileStyles.container} onContextMenu={handleContextMenu}> <div style={containerStyles} className={attachmentFileStyles.container} onContextMenu={handleContextMenu}>
{canDelete && ( {showDeleteButton && (
<button <button
type="button" type="button"
onClick={handleDelete} onClick={handleDelete}

View File

@ -37,6 +37,7 @@ import EmbedVideo from '~/components/channel/embeds/media/EmbedVideo';
import {getMediaButtonVisibility} from '~/components/channel/embeds/media/MediaButtonUtils'; import {getMediaButtonVisibility} from '~/components/channel/embeds/media/MediaButtonUtils';
import {MediaContainer} from '~/components/channel/embeds/media/MediaContainer'; import {MediaContainer} from '~/components/channel/embeds/media/MediaContainer';
import {NSFWBlurOverlay} from '~/components/channel/embeds/NSFWBlurOverlay'; import {NSFWBlurOverlay} from '~/components/channel/embeds/NSFWBlurOverlay';
import {useMaybeMessageViewContext} from '~/components/channel/MessageViewContext';
import {ExpiryFootnote} from '~/components/common/ExpiryFootnote'; import {ExpiryFootnote} from '~/components/common/ExpiryFootnote';
import {SpoilerOverlay} from '~/components/common/SpoilerOverlay'; import {SpoilerOverlay} from '~/components/common/SpoilerOverlay';
import {AddFavoriteMemeModal} from '~/components/modals/AddFavoriteMemeModal'; import {AddFavoriteMemeModal} from '~/components/modals/AddFavoriteMemeModal';
@ -61,12 +62,14 @@ interface AttachmentMosaicProps {
attachments: ReadonlyArray<MessageAttachment>; attachments: ReadonlyArray<MessageAttachment>;
message?: MessageRecord; message?: MessageRecord;
hideExpiryFootnote?: boolean; hideExpiryFootnote?: boolean;
isPreview?: boolean;
} }
interface SingleAttachmentProps { interface SingleAttachmentProps {
attachment: MessageAttachment; attachment: MessageAttachment;
message?: MessageRecord; message?: MessageRecord;
mediaAttachments: ReadonlyArray<MessageAttachment>; mediaAttachments: ReadonlyArray<MessageAttachment>;
isPreview?: boolean;
} }
const isImageType = (contentType?: string): boolean => contentType?.startsWith('image/') ?? false; const isImageType = (contentType?: string): boolean => contentType?.startsWith('image/') ?? false;
@ -122,10 +125,13 @@ interface MosaicItemProps {
style?: CSSProperties; style?: CSSProperties;
message?: MessageRecord; message?: MessageRecord;
mediaAttachments?: ReadonlyArray<MessageAttachment>; mediaAttachments?: ReadonlyArray<MessageAttachment>;
isPreview?: boolean;
} }
const MosaicItemBase: FC<MosaicItemProps> = observer(({attachment, style, message, mediaAttachments = []}) => { const MosaicItemBase: FC<MosaicItemProps> = observer(
({attachment, style, message, mediaAttachments = [], isPreview}) => {
const {i18n} = useLingui(); const {i18n} = useLingui();
const messageViewContext = useMaybeMessageViewContext();
const isVideo = isVideoType(attachment.content_type); const isVideo = isVideoType(attachment.content_type);
const isAudio = isAudioType(attachment.content_type); const isAudio = isAudioType(attachment.content_type);
const isAnimatedGif = isAnimated(attachment.flags) || isGifType(attachment.content_type); const isAnimatedGif = isAnimated(attachment.flags) || isGifType(attachment.content_type);
@ -273,11 +279,13 @@ const MosaicItemBase: FC<MosaicItemProps> = observer(({attachment, style, messag
[attachment.url, isAudio, isVideo], [attachment.url, isAudio, isVideo],
); );
const handleDeleteClick = useDeleteAttachment(message, attachment.id); const isRealAttachment = !message?.attachments ? false : message.attachments.some((a) => a.id === attachment.id);
const handleDeleteClick = useDeleteAttachment(message, isRealAttachment ? attachment.id : undefined);
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(e: MouseEvent) => { (e: MouseEvent) => {
if (!message) return; if (!message || isPreview) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -296,11 +304,11 @@ const MosaicItemBase: FC<MosaicItemProps> = observer(({attachment, style, messag
defaultName={defaultName} defaultName={defaultName}
defaultAltText={attachment.filename} defaultAltText={attachment.filename}
onClose={onClose} onClose={onClose}
onDelete={() => {}} onDelete={isPreview ? () => {} : (messageViewContext?.handleDelete ?? (() => {}))}
/> />
)); ));
}, },
[message, attachment, isAudio, isVideo], [message, attachment, isAudio, isVideo, isPreview, messageViewContext],
); );
const mediaType = isAudio ? 'audio' : isVideo ? 'video' : isAnimatedGif ? 'animated GIF' : 'image'; const mediaType = isAudio ? 'audio' : isVideo ? 'video' : isAnimatedGif ? 'animated GIF' : 'image';
@ -312,8 +320,9 @@ const MosaicItemBase: FC<MosaicItemProps> = observer(({attachment, style, messag
const canFavorite = !!(message?.channelId && message?.id); const canFavorite = !!(message?.channelId && message?.id);
const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility( const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility(
canFavorite, canFavorite,
message, isPreview ? undefined : message,
attachment.id, isRealAttachment ? attachment.id : undefined,
{disableDelete: !!isPreview},
); );
return wrapSpoiler( return wrapSpoiler(
@ -393,9 +402,10 @@ const MosaicItemBase: FC<MosaicItemProps> = observer(({attachment, style, messag
</button> </button>
</MediaContainer>, </MediaContainer>,
); );
}); },
);
const SingleAttachment: FC<SingleAttachmentProps> = observer(({attachment, message, mediaAttachments}) => { const SingleAttachment: FC<SingleAttachmentProps> = observer(({attachment, message, mediaAttachments, isPreview}) => {
const isVideo = isVideoType(attachment.content_type); const isVideo = isVideoType(attachment.content_type);
const isAnimatedGif = isAnimated(attachment.flags) || isGifType(attachment.content_type); const isAnimatedGif = isAnimated(attachment.flags) || isGifType(attachment.content_type);
const isSpoiler = (attachment.flags & MessageAttachmentFlags.IS_SPOILER) !== 0; const isSpoiler = (attachment.flags & MessageAttachmentFlags.IS_SPOILER) !== 0;
@ -452,6 +462,7 @@ const SingleAttachment: FC<SingleAttachmentProps> = observer(({attachment, messa
height={dimensions.height} height={dimensions.height}
title={attachment.title || attachment.filename} title={attachment.title || attachment.filename}
mediaAttachments={mediaAttachments} mediaAttachments={mediaAttachments}
isPreview={isPreview}
/> />
</div> </div>
</div> </div>
@ -474,6 +485,7 @@ const SingleAttachment: FC<SingleAttachmentProps> = observer(({attachment, messa
proxyURL={animatedProxyURL} proxyURL={animatedProxyURL}
naturalWidth={attachment.width!} naturalWidth={attachment.width!}
naturalHeight={attachment.height!} naturalHeight={attachment.height!}
isPreview={isPreview}
/> />
</div> </div>
</div> </div>
@ -501,11 +513,13 @@ const SingleAttachment: FC<SingleAttachmentProps> = observer(({attachment, messa
height={dimensions.height} height={dimensions.height}
constrain={true} constrain={true}
mediaAttachments={mediaAttachments} mediaAttachments={mediaAttachments}
isPreview={isPreview}
/>, />,
); );
}); });
const AttachmentMosaicComponent: FC<AttachmentMosaicProps> = observer(({attachments, message, hideExpiryFootnote}) => { const AttachmentMosaicComponent: FC<AttachmentMosaicProps> = observer(
({attachments, message, hideExpiryFootnote, isPreview}) => {
const {t} = useLingui(); const {t} = useLingui();
const mediaAttachments = attachments.filter(isMediaAttachment); const mediaAttachments = attachments.filter(isMediaAttachment);
@ -546,6 +560,7 @@ const AttachmentMosaicComponent: FC<AttachmentMosaicProps> = observer(({attachme
attachment={attachment} attachment={attachment}
message={message} message={message}
mediaAttachments={mediaAttachments} mediaAttachments={mediaAttachments}
isPreview={isPreview}
/> />
); );
@ -563,7 +578,12 @@ const AttachmentMosaicComponent: FC<AttachmentMosaicProps> = observer(({attachme
return ( return (
<div className={styles.mosaicContainerWrapper}> <div className={styles.mosaicContainerWrapper}>
<div className={styles.mosaicContainer}> <div className={styles.mosaicContainer}>
<SingleAttachment attachment={mediaAttachments[0]} message={message} mediaAttachments={mediaAttachments} /> <SingleAttachment
attachment={mediaAttachments[0]}
message={message}
mediaAttachments={mediaAttachments}
isPreview={isPreview}
/>
</div> </div>
{renderFootnote()} {renderFootnote()}
</div> </div>
@ -679,6 +699,7 @@ const AttachmentMosaicComponent: FC<AttachmentMosaicProps> = observer(({attachme
default: default:
throw new Error('This should never happen'); throw new Error('This should never happen');
} }
}); },
);
export const AttachmentMosaic: FC<AttachmentMosaicProps> = AttachmentMosaicComponent; export const AttachmentMosaic: FC<AttachmentMosaicProps> = AttachmentMosaicComponent;

View File

@ -25,8 +25,8 @@ import {type FC, useCallback, useEffect, useRef, useState} from 'react';
import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators'; import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators';
import * as MediaViewerActionCreators from '~/actions/MediaViewerActionCreators'; import * as MediaViewerActionCreators from '~/actions/MediaViewerActionCreators';
import {deriveDefaultNameFromMessage, splitFilename} from '~/components/channel/embeds/EmbedUtils'; import {deriveDefaultNameFromMessage, splitFilename} from '~/components/channel/embeds/EmbedUtils';
import {getMediaButtonVisibility} from '~/components/channel/embeds/media/MediaButtonUtils';
import type {BaseMediaProps} from '~/components/channel/embeds/media/MediaTypes'; import type {BaseMediaProps} from '~/components/channel/embeds/media/MediaTypes';
import {canDeleteAttachmentUtil} from '~/components/channel/messageActionUtils';
import {InlineAudioPlayer} from '~/components/media-player/components/InlineAudioPlayer'; import {InlineAudioPlayer} from '~/components/media-player/components/InlineAudioPlayer';
import {MediaContextMenu} from '~/components/uikit/ContextMenu/MediaContextMenu'; import {MediaContextMenu} from '~/components/uikit/ContextMenu/MediaContextMenu';
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip'; import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
@ -47,6 +47,7 @@ type EmbedAudioProps = BaseMediaProps & {
embedUrl?: string; embedUrl?: string;
fileSize?: number; fileSize?: number;
mediaAttachments?: ReadonlyArray<MessageAttachment>; mediaAttachments?: ReadonlyArray<MessageAttachment>;
isPreview?: boolean;
}; };
const EmbedAudio: FC<EmbedAudioProps> = observer( const EmbedAudio: FC<EmbedAudioProps> = observer(
@ -63,6 +64,7 @@ const EmbedAudio: FC<EmbedAudioProps> = observer(
contentHash, contentHash,
onDelete, onDelete,
fileSize, fileSize,
isPreview,
}) => { }) => {
const {t} = useLingui(); const {t} = useLingui();
const effectiveSrc = buildMediaProxyURL(src); const effectiveSrc = buildMediaProxyURL(src);
@ -88,7 +90,7 @@ const EmbedAudio: FC<EmbedAudioProps> = observer(
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
if (!message) return; if (!message || isPreview) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -106,7 +108,7 @@ const EmbedAudio: FC<EmbedAudioProps> = observer(
/> />
)); ));
}, },
[message, src, contentHash, attachmentId, defaultName, onDelete], [message, src, contentHash, attachmentId, defaultName, onDelete, isPreview],
); );
useEffect(() => { useEffect(() => {
@ -189,7 +191,12 @@ const EmbedAudio: FC<EmbedAudioProps> = observer(
maxWidth: '400px', maxWidth: '400px',
}; };
const canDelete = canDeleteAttachmentUtil(message) && !isMobile; const {showDeleteButton, showDownloadButton} = getMediaButtonVisibility(
canFavorite,
isPreview ? undefined : message,
attachmentId,
{disableDelete: !!isPreview},
);
if (isMobile) { if (isMobile) {
return ( return (
@ -223,7 +230,7 @@ const EmbedAudio: FC<EmbedAudioProps> = observer(
return ( return (
<div style={containerStyles} className={styles.container}> <div style={containerStyles} className={styles.container}>
{canDelete && ( {showDeleteButton && (
<Tooltip text={t`Delete`} position="top"> <Tooltip text={t`Delete`} position="top">
<button <button
type="button" type="button"
@ -243,7 +250,7 @@ const EmbedAudio: FC<EmbedAudioProps> = observer(
isFavorited={isFavorited} isFavorited={isFavorited}
canFavorite={canFavorite} canFavorite={canFavorite}
onFavoriteClick={handleFavoriteClick} onFavoriteClick={handleFavoriteClick}
onDownloadClick={handleDownload} onDownloadClick={showDownloadButton ? handleDownload : undefined}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
/> />
</div> </div>

View File

@ -255,6 +255,7 @@ export const EmbedGifv: FC<
videoProxyURL: string; videoProxyURL: string;
videoURL: string; videoURL: string;
videoConfig?: VideoConfig; videoConfig?: VideoConfig;
isPreview?: boolean;
} }
> = observer( > = observer(
({ ({
@ -272,6 +273,7 @@ export const EmbedGifv: FC<
message, message,
contentHash, contentHash,
onDelete, onDelete,
isPreview,
}) => { }) => {
const {t} = useLingui(); const {t} = useLingui();
const {loaded, error, thumbHashURL} = useMediaLoading( const {loaded, error, thumbHashURL} = useMediaLoading(
@ -329,7 +331,7 @@ export const EmbedGifv: FC<
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
if (!message) return; if (!message || isPreview) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -348,7 +350,7 @@ export const EmbedGifv: FC<
/> />
)); ));
}, },
[message, embedURL, videoProxyURL, contentHash, attachmentId, defaultName, onDelete], [message, embedURL, videoProxyURL, contentHash, attachmentId, defaultName, onDelete, isPreview],
); );
useEffect(() => { useEffect(() => {
@ -449,7 +451,9 @@ export const EmbedGifv: FC<
showFavoriteButton, showFavoriteButton,
showDownloadButton: _showDownloadButton, showDownloadButton: _showDownloadButton,
showDeleteButton, showDeleteButton,
} = getMediaButtonVisibility(canFavorite, message, attachmentId); } = getMediaButtonVisibility(canFavorite, isPreview ? undefined : message, attachmentId, {
disableDelete: !!isPreview,
});
const showDownloadButton = false; const showDownloadButton = false;
const showGifIndicator = const showGifIndicator =
AccessibilityStore.showGifIndicator && shouldShowOverlays(dimensions.width, dimensions.height); AccessibilityStore.showGifIndicator && shouldShowOverlays(dimensions.width, dimensions.height);
@ -515,7 +519,7 @@ export const EmbedGifv: FC<
}, },
); );
export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: boolean}> = observer( export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: boolean; isPreview?: boolean}> = observer(
({ ({
embedURL, embedURL,
proxyURL, proxyURL,
@ -530,6 +534,7 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
message, message,
contentHash, contentHash,
onDelete, onDelete,
isPreview,
}) => { }) => {
const {t} = useLingui(); const {t} = useLingui();
const {dimensions} = mediaCalculator.calculate({width: naturalWidth, height: naturalHeight}, {forceScale: true}); const {dimensions} = mediaCalculator.calculate({width: naturalWidth, height: naturalHeight}, {forceScale: true});
@ -601,7 +606,7 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
if (!message) return; if (!message || isPreview) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -620,7 +625,7 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
/> />
)); ));
}, },
[message, embedURL, proxyURL, contentHash, attachmentId, defaultName, onDelete], [message, embedURL, proxyURL, contentHash, attachmentId, defaultName, onDelete, isPreview],
); );
useEffect(() => { useEffect(() => {
@ -697,8 +702,9 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
); );
const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility( const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility(
canFavorite, canFavorite,
message, isPreview ? undefined : message,
attachmentId, attachmentId,
{disableDelete: !!isPreview},
); );
const showGifIndicator = const showGifIndicator =
AccessibilityStore.showGifIndicator && shouldShowOverlays(renderedDimensions.width, renderedDimensions.height); AccessibilityStore.showGifIndicator && shouldShowOverlays(renderedDimensions.width, renderedDimensions.height);

View File

@ -77,6 +77,7 @@ type EmbedImageProps = React.ImgHTMLAttributes<HTMLImageElement> &
handlePress?: (event: React.MouseEvent | React.KeyboardEvent) => void; handlePress?: (event: React.MouseEvent | React.KeyboardEvent) => void;
alt?: string; alt?: string;
mediaAttachments?: ReadonlyArray<MessageAttachment>; mediaAttachments?: ReadonlyArray<MessageAttachment>;
isPreview?: boolean;
}; };
const ImagePreviewHandler: FC<ImagePreviewHandlerProps> = observer( const ImagePreviewHandler: FC<ImagePreviewHandlerProps> = observer(
@ -216,6 +217,7 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
contentHash, contentHash,
onDelete, onDelete,
mediaAttachments = [], mediaAttachments = [],
isPreview,
}) => { }) => {
const {t} = useLingui(); const {t} = useLingui();
const {loaded, error, thumbHashURL} = useMediaLoading(src, placeholder); const {loaded, error, thumbHashURL} = useMediaLoading(src, placeholder);
@ -271,7 +273,7 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
if (!message) return; if (!message || isPreview) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -292,7 +294,7 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
/> />
)); ));
}, },
[message, src, originalSrc, contentHash, attachmentId, embedIndex, alt, defaultName, onDelete], [message, src, originalSrc, contentHash, attachmentId, embedIndex, alt, defaultName, onDelete, isPreview],
); );
if (shouldBlur) { if (shouldBlur) {
@ -324,8 +326,9 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility( const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility(
canFavorite, canFavorite,
message, isPreview ? undefined : message,
attachmentId, attachmentId,
{disableDelete: !!isPreview},
); );
return ( return (

View File

@ -65,6 +65,7 @@ type EmbedVideoProps = BaseMediaProps & {
embedUrl?: string; embedUrl?: string;
fillContainer?: boolean; fillContainer?: boolean;
mediaAttachments?: ReadonlyArray<MessageAttachment>; mediaAttachments?: ReadonlyArray<MessageAttachment>;
isPreview?: boolean;
}; };
const MobileVideoOverlay: FC<{ const MobileVideoOverlay: FC<{
@ -125,6 +126,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
onDelete, onDelete,
fillContainer = false, fillContainer = false,
mediaAttachments = [], mediaAttachments = [],
isPreview,
}) => { }) => {
const {enabled: isMobile} = MobileLayoutStore; const {enabled: isMobile} = MobileLayoutStore;
const effectiveSrc = buildMediaProxyURL(src); const effectiveSrc = buildMediaProxyURL(src);
@ -162,7 +164,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
if (!message) return; if (!message || isPreview) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -180,7 +182,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
/> />
)); ));
}, },
[message, src, contentHash, attachmentId, defaultName, onDelete], [message, src, contentHash, attachmentId, defaultName, onDelete, isPreview],
); );
const thumbHashUrl = placeholder const thumbHashUrl = placeholder
@ -268,8 +270,9 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility( const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility(
canFavorite, canFavorite,
message, isPreview ? undefined : message,
attachmentId, attachmentId,
{disableDelete: !!isPreview},
); );
if (isMobile) { if (isMobile) {

View File

@ -21,6 +21,10 @@ import {canDeleteAttachmentUtil} from '~/components/channel/messageActionUtils';
import type {MessageRecord} from '~/records/MessageRecord'; import type {MessageRecord} from '~/records/MessageRecord';
import AccessibilityStore from '~/stores/AccessibilityStore'; import AccessibilityStore from '~/stores/AccessibilityStore';
export interface MediaButtonVisibilityOptions {
disableDelete?: boolean;
}
export interface MediaButtonVisibility { export interface MediaButtonVisibility {
showFavoriteButton: boolean; showFavoriteButton: boolean;
showDownloadButton: boolean; showDownloadButton: boolean;
@ -31,14 +35,17 @@ export function getMediaButtonVisibility(
canFavorite: boolean, canFavorite: boolean,
message?: MessageRecord, message?: MessageRecord,
attachmentId?: string, attachmentId?: string,
options?: MediaButtonVisibilityOptions,
): MediaButtonVisibility { ): MediaButtonVisibility {
const showMediaFavoriteButton = AccessibilityStore.showMediaFavoriteButton; const showMediaFavoriteButton = AccessibilityStore.showMediaFavoriteButton;
const showMediaDownloadButton = AccessibilityStore.showMediaDownloadButton; const showMediaDownloadButton = AccessibilityStore.showMediaDownloadButton;
const showMediaDeleteButton = AccessibilityStore.showMediaDeleteButton; const showMediaDeleteButton = AccessibilityStore.showMediaDeleteButton;
const disableDelete = options?.disableDelete ?? false;
return { return {
showFavoriteButton: showMediaFavoriteButton && canFavorite, showFavoriteButton: showMediaFavoriteButton && canFavorite,
showDownloadButton: showMediaDownloadButton, showDownloadButton: showMediaDownloadButton,
showDeleteButton: showMediaDeleteButton && !!(message && attachmentId && canDeleteAttachmentUtil(message)), showDeleteButton:
showMediaDeleteButton && !disableDelete && !!(message && attachmentId && canDeleteAttachmentUtil(message)),
}; };
} }