fix(embed): hide remove buttons when unapplicable (#66)
This commit is contained in:
parent
c50a74db7b
commit
056d578965
@ -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}
|
||||||
/>
|
/>
|
||||||
@ -193,23 +196,24 @@ const ForwardedMessageContent = observer(({message, snapshot}: {message: Message
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{snapshot.embeds && snapshot.embeds.length > 0 && UserSettingsStore.getRenderEmbeds() && (
|
{snapshot.embeds && snapshot.embeds.length > 0 && UserSettingsStore.getRenderEmbeds() && (
|
||||||
<div className={styles.attachmentsContainer}>
|
<div className={styles.attachmentsContainer}>
|
||||||
{snapshot.embeds.map((embed: MessageEmbed, index: number) => {
|
{snapshot.embeds.map((embed: MessageEmbed, index: number) => {
|
||||||
const embedKey = `${embed.id}-${index}`;
|
const embedKey = `${embed.id}-${index}`;
|
||||||
return (
|
return (
|
||||||
<Embed
|
<Embed
|
||||||
embed={embed}
|
embed={embed}
|
||||||
key={embedKey}
|
key={embedKey}
|
||||||
message={message}
|
message={message}
|
||||||
embedIndex={index}
|
embedIndex={index}
|
||||||
contextualEmbeds={snapshot.embeds}
|
contextualEmbeds={snapshot.embeds}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
/>
|
isPreview={snapshotIsPreview}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
</div>
|
})}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ForwardedFromSource message={message} />
|
<ForwardedFromSource message={message} />
|
||||||
</div>
|
</div>
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
@ -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>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -67,35 +67,38 @@ 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(
|
||||||
const embedUrl = attachment.url ?? '';
|
({attachment, message, isPreview}) => {
|
||||||
const proxyUrl = attachment.proxy_url ?? embedUrl;
|
const embedUrl = attachment.url ?? '';
|
||||||
const animatedProxyURL = buildMediaProxyURL(proxyUrl, {
|
const proxyUrl = attachment.proxy_url ?? embedUrl;
|
||||||
animated: true,
|
const animatedProxyURL = buildMediaProxyURL(proxyUrl, {
|
||||||
});
|
animated: true,
|
||||||
const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0;
|
});
|
||||||
|
const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusRing within ringClassName={messageStyles.mediaFocusRing}>
|
<FocusRing within ringClassName={messageStyles.mediaFocusRing}>
|
||||||
<EmbedGif
|
<EmbedGif
|
||||||
embedURL={embedUrl}
|
embedURL={embedUrl}
|
||||||
proxyURL={animatedProxyURL}
|
proxyURL={animatedProxyURL}
|
||||||
naturalWidth={attachment.width!}
|
naturalWidth={attachment.width!}
|
||||||
naturalHeight={attachment.height!}
|
naturalHeight={attachment.height!}
|
||||||
placeholder={attachment.placeholder}
|
placeholder={attachment.placeholder}
|
||||||
nsfw={nsfw}
|
nsfw={nsfw}
|
||||||
channelId={message?.channelId}
|
channelId={message?.channelId}
|
||||||
messageId={message?.id}
|
messageId={message?.id}
|
||||||
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,29 +143,32 @@ 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(
|
||||||
<FocusRing within ringClassName={messageStyles.mediaFocusRing}>
|
({attachment, message, isPreview}) => (
|
||||||
<div className={styles.attachmentWrapper}>
|
<FocusRing within ringClassName={messageStyles.mediaFocusRing}>
|
||||||
<EmbedAudio
|
<div className={styles.attachmentWrapper}>
|
||||||
src={attachment.proxy_url ?? attachment.url ?? ''}
|
<EmbedAudio
|
||||||
title={attachment.title || attachment.filename}
|
src={attachment.proxy_url ?? attachment.url ?? ''}
|
||||||
duration={attachment.duration}
|
title={attachment.title || attachment.filename}
|
||||||
embedUrl={attachment.url ?? ''}
|
duration={attachment.duration}
|
||||||
channelId={message?.channelId}
|
embedUrl={attachment.url ?? ''}
|
||||||
messageId={message?.id}
|
channelId={message?.channelId}
|
||||||
attachmentId={attachment.id}
|
messageId={message?.id}
|
||||||
message={message}
|
attachmentId={attachment.id}
|
||||||
contentHash={attachment.content_hash}
|
message={message}
|
||||||
/>
|
contentHash={attachment.content_hash}
|
||||||
</div>
|
isPreview={isPreview}
|
||||||
</FocusRing>
|
/>
|
||||||
));
|
</div>
|
||||||
|
</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>,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,280 +125,287 @@ 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(
|
||||||
const {i18n} = useLingui();
|
({attachment, style, message, mediaAttachments = [], isPreview}) => {
|
||||||
const isVideo = isVideoType(attachment.content_type);
|
const {i18n} = useLingui();
|
||||||
const isAudio = isAudioType(attachment.content_type);
|
const messageViewContext = useMaybeMessageViewContext();
|
||||||
const isAnimatedGif = isAnimated(attachment.flags) || isGifType(attachment.content_type);
|
const isVideo = isVideoType(attachment.content_type);
|
||||||
const isSpoiler = (attachment.flags & MessageAttachmentFlags.IS_SPOILER) !== 0;
|
const isAudio = isAudioType(attachment.content_type);
|
||||||
const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0;
|
const isAnimatedGif = isAnimated(attachment.flags) || isGifType(attachment.content_type);
|
||||||
|
const isSpoiler = (attachment.flags & MessageAttachmentFlags.IS_SPOILER) !== 0;
|
||||||
|
const nsfw = attachment.nsfw || (attachment.flags & MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA) !== 0;
|
||||||
|
|
||||||
const {hidden: spoilerHidden, reveal: revealSpoiler} = useSpoilerState(isSpoiler, message?.channelId);
|
const {hidden: spoilerHidden, reveal: revealSpoiler} = useSpoilerState(isSpoiler, message?.channelId);
|
||||||
const {shouldBlur, gateReason} = useNSFWMedia(nsfw, message?.channelId);
|
const {shouldBlur, gateReason} = useNSFWMedia(nsfw, message?.channelId);
|
||||||
|
|
||||||
const wrapSpoiler = (node: ReactElement) =>
|
const wrapSpoiler = (node: ReactElement) =>
|
||||||
isSpoiler ? (
|
isSpoiler ? (
|
||||||
<SpoilerOverlay hidden={spoilerHidden} onReveal={revealSpoiler}>
|
<SpoilerOverlay hidden={spoilerHidden} onReveal={revealSpoiler}>
|
||||||
{node}
|
{node}
|
||||||
</SpoilerOverlay>
|
</SpoilerOverlay>
|
||||||
) : (
|
) : (
|
||||||
node
|
node
|
||||||
);
|
);
|
||||||
|
|
||||||
const mosaicDimensions = getMosaicMediaDimensions(message);
|
const mosaicDimensions = getMosaicMediaDimensions(message);
|
||||||
const maxMosaicWidth = mosaicDimensions.maxWidth;
|
const maxMosaicWidth = mosaicDimensions.maxWidth;
|
||||||
const targetWidth = Math.min(attachment.width || maxMosaicWidth, maxMosaicWidth * 2);
|
const targetWidth = Math.min(attachment.width || maxMosaicWidth, maxMosaicWidth * 2);
|
||||||
const targetHeight = attachment.height
|
const targetHeight = attachment.height
|
||||||
? Math.round((targetWidth / attachment.width!) * attachment.height)
|
? Math.round((targetWidth / attachment.width!) * attachment.height)
|
||||||
: targetWidth;
|
: targetWidth;
|
||||||
|
|
||||||
const proxyUrl = attachment.proxy_url ?? attachment.url ?? '';
|
const proxyUrl = attachment.proxy_url ?? attachment.url ?? '';
|
||||||
const isBlob = proxyUrl.startsWith('blob:');
|
const isBlob = proxyUrl.startsWith('blob:');
|
||||||
|
|
||||||
const thumbnailSrc =
|
const thumbnailSrc =
|
||||||
proxyUrl.length === 0
|
proxyUrl.length === 0
|
||||||
? ''
|
? ''
|
||||||
: isBlob
|
: isBlob
|
||||||
? proxyUrl
|
|
||||||
: isAnimatedGif
|
|
||||||
? proxyUrl
|
? proxyUrl
|
||||||
: buildMediaProxyURL(proxyUrl, {
|
: isAnimatedGif
|
||||||
format: 'webp',
|
? proxyUrl
|
||||||
width: targetWidth,
|
: buildMediaProxyURL(proxyUrl, {
|
||||||
height: targetHeight,
|
format: 'webp',
|
||||||
});
|
width: targetWidth,
|
||||||
|
height: targetHeight,
|
||||||
|
});
|
||||||
|
|
||||||
const {loaded, error, thumbHashURL} = useMediaLoading(thumbnailSrc, attachment.placeholder);
|
const {loaded, error, thumbHashURL} = useMediaLoading(thumbnailSrc, attachment.placeholder);
|
||||||
|
|
||||||
const memes = FavoriteMemeStore.memes;
|
const memes = FavoriteMemeStore.memes;
|
||||||
const isFavorited = attachment.content_hash
|
const isFavorited = attachment.content_hash
|
||||||
? memes.some((meme) => meme.contentHash === attachment.content_hash)
|
? memes.some((meme) => meme.contentHash === attachment.content_hash)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(event: MouseEvent | KeyboardEvent) => {
|
(event: MouseEvent | KeyboardEvent) => {
|
||||||
if (shouldBlur) {
|
if (shouldBlur) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === 'keydown') {
|
|
||||||
const keyEvent = event as KeyboardEvent;
|
|
||||||
if (keyEvent.key !== 'Enter' && keyEvent.key !== ' ') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = mediaAttachments.findIndex((a) => a.id === attachment.id);
|
if (event.type === 'keydown') {
|
||||||
|
const keyEvent = event as KeyboardEvent;
|
||||||
const items = mediaAttachments.map((att) => {
|
if (keyEvent.key !== 'Enter' && keyEvent.key !== ' ') {
|
||||||
const attIsVideo = isVideoType(att.content_type);
|
return;
|
||||||
const attIsAudio = isAudioType(att.content_type);
|
}
|
||||||
const attIsAnimatedGif =
|
event.preventDefault();
|
||||||
(att.flags & MessageAttachmentFlags.IS_ANIMATED) !== 0 || att.content_type === 'image/gif';
|
|
||||||
|
|
||||||
let type: 'image' | 'gif' | 'gifv' | 'video' | 'audio';
|
|
||||||
if (attIsAudio) {
|
|
||||||
type = 'audio';
|
|
||||||
} else if (attIsVideo) {
|
|
||||||
type = 'video';
|
|
||||||
} else if (attIsAnimatedGif) {
|
|
||||||
type = 'gif';
|
|
||||||
} else {
|
|
||||||
type = 'image';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const attProxy = att.proxy_url ?? att.url ?? '';
|
const currentIndex = mediaAttachments.findIndex((a) => a.id === attachment.id);
|
||||||
const attUrl = att.url ?? '';
|
|
||||||
|
|
||||||
return {
|
const items = mediaAttachments.map((att) => {
|
||||||
src: attProxy,
|
const attIsVideo = isVideoType(att.content_type);
|
||||||
originalSrc: attUrl,
|
const attIsAudio = isAudioType(att.content_type);
|
||||||
naturalWidth: att.width || 0,
|
const attIsAnimatedGif =
|
||||||
naturalHeight: att.height || 0,
|
(att.flags & MessageAttachmentFlags.IS_ANIMATED) !== 0 || att.content_type === 'image/gif';
|
||||||
type,
|
|
||||||
contentHash: att.content_hash,
|
|
||||||
attachmentId: att.id,
|
|
||||||
filename: att.filename,
|
|
||||||
fileSize: att.size,
|
|
||||||
duration: att.duration,
|
|
||||||
expiresAt: att.expires_at ?? null,
|
|
||||||
expired: att.expired ?? false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
MediaViewerActionCreators.openMediaViewer(items, currentIndex, {
|
let type: 'image' | 'gif' | 'gifv' | 'video' | 'audio';
|
||||||
channelId: message?.channelId,
|
if (attIsAudio) {
|
||||||
messageId: message?.id,
|
type = 'audio';
|
||||||
message,
|
} else if (attIsVideo) {
|
||||||
});
|
type = 'video';
|
||||||
},
|
} else if (attIsAnimatedGif) {
|
||||||
[attachment, message, mediaAttachments, shouldBlur],
|
type = 'gif';
|
||||||
);
|
} else {
|
||||||
|
type = 'image';
|
||||||
|
}
|
||||||
|
|
||||||
const handleFavoriteClick = useCallback(
|
const attProxy = att.proxy_url ?? att.url ?? '';
|
||||||
async (e: MouseEvent) => {
|
const attUrl = att.url ?? '';
|
||||||
e.stopPropagation();
|
|
||||||
if (!message?.channelId || !message?.id) return;
|
|
||||||
|
|
||||||
if (isFavorited && attachment.content_hash) {
|
return {
|
||||||
const meme = memes.find((m) => m.contentHash === attachment.content_hash);
|
src: attProxy,
|
||||||
if (!meme) return;
|
originalSrc: attUrl,
|
||||||
await FavoriteMemeActionCreators.deleteFavoriteMeme(i18n, meme.id);
|
naturalWidth: att.width || 0,
|
||||||
} else {
|
naturalHeight: att.height || 0,
|
||||||
|
type,
|
||||||
|
contentHash: att.content_hash,
|
||||||
|
attachmentId: att.id,
|
||||||
|
filename: att.filename,
|
||||||
|
fileSize: att.size,
|
||||||
|
duration: att.duration,
|
||||||
|
expiresAt: att.expires_at ?? null,
|
||||||
|
expired: att.expired ?? false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
MediaViewerActionCreators.openMediaViewer(items, currentIndex, {
|
||||||
|
channelId: message?.channelId,
|
||||||
|
messageId: message?.id,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[attachment, message, mediaAttachments, shouldBlur],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFavoriteClick = useCallback(
|
||||||
|
async (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!message?.channelId || !message?.id) return;
|
||||||
|
|
||||||
|
if (isFavorited && attachment.content_hash) {
|
||||||
|
const meme = memes.find((m) => m.contentHash === attachment.content_hash);
|
||||||
|
if (!meme) return;
|
||||||
|
await FavoriteMemeActionCreators.deleteFavoriteMeme(i18n, meme.id);
|
||||||
|
} else {
|
||||||
|
const defaultName = FavoriteMemeUtils.deriveDefaultNameFromAttachment(i18n, attachment);
|
||||||
|
ModalActionCreators.push(
|
||||||
|
modal(() => (
|
||||||
|
<AddFavoriteMemeModal
|
||||||
|
channelId={message.channelId}
|
||||||
|
messageId={message.id}
|
||||||
|
attachmentId={attachment.id}
|
||||||
|
defaultName={defaultName}
|
||||||
|
defaultAltText={attachment.filename}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[message, attachment, isFavorited, memes, i18n],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDownloadClick = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const mediaType = isAudio ? 'audio' : isVideo ? 'video' : 'image';
|
||||||
|
createSaveHandler(attachment.url ?? '', mediaType)();
|
||||||
|
},
|
||||||
|
[attachment.url, isAudio, isVideo],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isRealAttachment = !message?.attachments ? false : message.attachments.some((a) => a.id === attachment.id);
|
||||||
|
|
||||||
|
const handleDeleteClick = useDeleteAttachment(message, isRealAttachment ? attachment.id : undefined);
|
||||||
|
|
||||||
|
const handleContextMenu = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (!message || isPreview) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const mediaType = isAudio ? 'audio' : isVideo ? 'video' : 'image';
|
||||||
const defaultName = FavoriteMemeUtils.deriveDefaultNameFromAttachment(i18n, attachment);
|
const defaultName = FavoriteMemeUtils.deriveDefaultNameFromAttachment(i18n, attachment);
|
||||||
ModalActionCreators.push(
|
|
||||||
modal(() => (
|
|
||||||
<AddFavoriteMemeModal
|
|
||||||
channelId={message.channelId}
|
|
||||||
messageId={message.id}
|
|
||||||
attachmentId={attachment.id}
|
|
||||||
defaultName={defaultName}
|
|
||||||
defaultAltText={attachment.filename}
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[message, attachment, isFavorited, memes, i18n],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDownloadClick = useCallback(
|
ContextMenuActionCreators.openFromEvent(e, ({onClose}) => (
|
||||||
(e: MouseEvent) => {
|
<MediaContextMenu
|
||||||
e.stopPropagation();
|
message={message}
|
||||||
const mediaType = isAudio ? 'audio' : isVideo ? 'video' : 'image';
|
originalSrc={attachment.url ?? ''}
|
||||||
createSaveHandler(attachment.url ?? '', mediaType)();
|
proxyURL={attachment.proxy_url ?? attachment.url ?? ''}
|
||||||
},
|
type={mediaType}
|
||||||
[attachment.url, isAudio, isVideo],
|
contentHash={attachment.content_hash}
|
||||||
);
|
attachmentId={attachment.id}
|
||||||
|
defaultName={defaultName}
|
||||||
|
defaultAltText={attachment.filename}
|
||||||
|
onClose={onClose}
|
||||||
|
onDelete={isPreview ? () => {} : (messageViewContext?.handleDelete ?? (() => {}))}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
},
|
||||||
|
[message, attachment, isAudio, isVideo, isPreview, messageViewContext],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeleteClick = useDeleteAttachment(message, attachment.id);
|
const mediaType = isAudio ? 'audio' : isVideo ? 'video' : isAnimatedGif ? 'animated GIF' : 'image';
|
||||||
|
const ariaLabel = `Open ${mediaType} in full view`;
|
||||||
|
const shouldRenderPlaceholder = !loaded || error;
|
||||||
|
const aspectRatioStyle =
|
||||||
|
attachment.width && attachment.height ? {aspectRatio: `${attachment.width} / ${attachment.height}`} : undefined;
|
||||||
|
|
||||||
const handleContextMenu = useCallback(
|
const canFavorite = !!(message?.channelId && message?.id);
|
||||||
(e: MouseEvent) => {
|
const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility(
|
||||||
if (!message) return;
|
canFavorite,
|
||||||
|
isPreview ? undefined : message,
|
||||||
|
isRealAttachment ? attachment.id : undefined,
|
||||||
|
{disableDelete: !!isPreview},
|
||||||
|
);
|
||||||
|
|
||||||
e.preventDefault();
|
return wrapSpoiler(
|
||||||
e.stopPropagation();
|
<MediaContainer
|
||||||
|
className={styles.mosaicItem}
|
||||||
const mediaType = isAudio ? 'audio' : isVideo ? 'video' : 'image';
|
style={style}
|
||||||
const defaultName = FavoriteMemeUtils.deriveDefaultNameFromAttachment(i18n, attachment);
|
showFavoriteButton={showFavoriteButton}
|
||||||
|
isFavorited={isFavorited}
|
||||||
ContextMenuActionCreators.openFromEvent(e, ({onClose}) => (
|
onFavoriteClick={handleFavoriteClick}
|
||||||
<MediaContextMenu
|
showDownloadButton={showDownloadButton}
|
||||||
message={message}
|
onDownloadClick={handleDownloadClick}
|
||||||
originalSrc={attachment.url ?? ''}
|
showDeleteButton={showDeleteButton}
|
||||||
proxyURL={attachment.proxy_url ?? attachment.url ?? ''}
|
onDeleteClick={handleDeleteClick}
|
||||||
type={mediaType}
|
onContextMenu={handleContextMenu}
|
||||||
contentHash={attachment.content_hash}
|
|
||||||
attachmentId={attachment.id}
|
|
||||||
defaultName={defaultName}
|
|
||||||
defaultAltText={attachment.filename}
|
|
||||||
onClose={onClose}
|
|
||||||
onDelete={() => {}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
},
|
|
||||||
[message, attachment, isAudio, isVideo],
|
|
||||||
);
|
|
||||||
|
|
||||||
const mediaType = isAudio ? 'audio' : isVideo ? 'video' : isAnimatedGif ? 'animated GIF' : 'image';
|
|
||||||
const ariaLabel = `Open ${mediaType} in full view`;
|
|
||||||
const shouldRenderPlaceholder = !loaded || error;
|
|
||||||
const aspectRatioStyle =
|
|
||||||
attachment.width && attachment.height ? {aspectRatio: `${attachment.width} / ${attachment.height}`} : undefined;
|
|
||||||
|
|
||||||
const canFavorite = !!(message?.channelId && message?.id);
|
|
||||||
const {showFavoriteButton, showDownloadButton, showDeleteButton} = getMediaButtonVisibility(
|
|
||||||
canFavorite,
|
|
||||||
message,
|
|
||||||
attachment.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return wrapSpoiler(
|
|
||||||
<MediaContainer
|
|
||||||
className={styles.mosaicItem}
|
|
||||||
style={style}
|
|
||||||
showFavoriteButton={showFavoriteButton}
|
|
||||||
isFavorited={isFavorited}
|
|
||||||
onFavoriteClick={handleFavoriteClick}
|
|
||||||
showDownloadButton={showDownloadButton}
|
|
||||||
onDownloadClick={handleDownloadClick}
|
|
||||||
showDeleteButton={showDeleteButton}
|
|
||||||
onDeleteClick={handleDeleteClick}
|
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.clickableButton}
|
|
||||||
onClick={handleClick}
|
|
||||||
onKeyDown={handleClick}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
>
|
>
|
||||||
{isAudio ? (
|
<button
|
||||||
<div className={styles.audioPlaceholder}>
|
type="button"
|
||||||
<SpeakerHighIcon weight="fill" />
|
className={styles.clickableButton}
|
||||||
</div>
|
onClick={handleClick}
|
||||||
) : (
|
onKeyDown={handleClick}
|
||||||
<div className={styles.mediaContainer}>
|
aria-label={ariaLabel}
|
||||||
<div className={styles.clickableWrapper}>
|
>
|
||||||
<div className={styles.loadingOverlay} style={aspectRatioStyle}>
|
{isAudio ? (
|
||||||
{isAnimatedGif && <div className={styles.gifIndicator}>GIF</div>}
|
<div className={styles.audioPlaceholder}>
|
||||||
|
<SpeakerHighIcon weight="fill" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.mediaContainer}>
|
||||||
|
<div className={styles.clickableWrapper}>
|
||||||
|
<div className={styles.loadingOverlay} style={aspectRatioStyle}>
|
||||||
|
{isAnimatedGif && <div className={styles.gifIndicator}>GIF</div>}
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{shouldRenderPlaceholder && thumbHashURL && (
|
{shouldRenderPlaceholder && thumbHashURL && (
|
||||||
<motion.img
|
<motion.img
|
||||||
key="placeholder"
|
key="placeholder"
|
||||||
initial={{opacity: 1}}
|
initial={{opacity: 1}}
|
||||||
exit={{opacity: 0}}
|
exit={{opacity: 0}}
|
||||||
transition={{duration: 0.3}}
|
transition={{duration: 0.3}}
|
||||||
src={thumbHashURL}
|
src={thumbHashURL}
|
||||||
alt=""
|
alt=""
|
||||||
className={styles.placeholderImage}
|
className={styles.placeholderImage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={thumbnailSrc}
|
src={thumbnailSrc}
|
||||||
alt={attachment.filename}
|
alt={attachment.filename}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
styles.mediaImage,
|
styles.mediaImage,
|
||||||
shouldRenderPlaceholder && styles.mediaImageHidden,
|
shouldRenderPlaceholder && styles.mediaImageHidden,
|
||||||
shouldBlur && styles.mediaBlurred,
|
shouldBlur && styles.mediaBlurred,
|
||||||
)}
|
)}
|
||||||
aria-hidden={shouldBlur}
|
aria-hidden={shouldBlur}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{shouldBlur && (
|
|
||||||
<div className={styles.nsfwOverlay}>
|
|
||||||
<NSFWBlurOverlay reason={gateReason} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(isVideo || isAudio) && (
|
{shouldBlur && (
|
||||||
<div className={styles.playButtonOverlay}>
|
<div className={styles.nsfwOverlay}>
|
||||||
<div className={styles.playButton}>
|
<NSFWBlurOverlay reason={gateReason} />
|
||||||
<PlayIcon size={28} weight="fill" aria-hidden="true" />
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</MediaContainer>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const SingleAttachment: FC<SingleAttachmentProps> = observer(({attachment, message, mediaAttachments}) => {
|
{(isVideo || isAudio) && (
|
||||||
|
<div className={styles.playButtonOverlay}>
|
||||||
|
<div className={styles.playButton}>
|
||||||
|
<PlayIcon size={28} weight="fill" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</MediaContainer>,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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,184 +513,193 @@ 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(
|
||||||
const {t} = useLingui();
|
({attachments, message, hideExpiryFootnote, isPreview}) => {
|
||||||
|
const {t} = useLingui();
|
||||||
|
|
||||||
const mediaAttachments = attachments.filter(isMediaAttachment);
|
const mediaAttachments = attachments.filter(isMediaAttachment);
|
||||||
const count = mediaAttachments.length;
|
const count = mediaAttachments.length;
|
||||||
const aggregateExpiry = getEarliestAttachmentExpiry(attachments);
|
const aggregateExpiry = getEarliestAttachmentExpiry(attachments);
|
||||||
|
|
||||||
const renderFootnote = () => {
|
const renderFootnote = () => {
|
||||||
if (hideExpiryFootnote) {
|
if (hideExpiryFootnote) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const earliest = formatAttachmentDate(aggregateExpiry.expiresAt);
|
||||||
|
const latest = formatAttachmentDate(aggregateExpiry.latestAt);
|
||||||
|
|
||||||
|
let label: string;
|
||||||
|
if (earliest && latest && earliest !== latest) {
|
||||||
|
label = aggregateExpiry.isExpired
|
||||||
|
? t`Expired between ${earliest} and ${latest}`
|
||||||
|
: t`Expires between ${earliest} and ${latest}`;
|
||||||
|
} else if (earliest) {
|
||||||
|
label = aggregateExpiry.isExpired ? t`Expired on ${earliest}` : t`Expires on ${earliest}`;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AccessibilityStore.showAttachmentExpiryIndicator ? (
|
||||||
|
<ExpiryFootnote expiresAt={aggregateExpiry.expiresAt} isExpired={aggregateExpiry.isExpired} label={label} />
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const earliest = formatAttachmentDate(aggregateExpiry.expiresAt);
|
const renderMosaicItem = (attachment: MessageAttachment, key?: string) => (
|
||||||
const latest = formatAttachmentDate(aggregateExpiry.latestAt);
|
<MosaicItemBase
|
||||||
|
key={key || attachment.id}
|
||||||
|
attachment={attachment}
|
||||||
|
message={message}
|
||||||
|
mediaAttachments={mediaAttachments}
|
||||||
|
isPreview={isPreview}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
let label: string;
|
const renderGrid = (items: ReadonlyArray<MessageAttachment>, gridClassName: string) => (
|
||||||
if (earliest && latest && earliest !== latest) {
|
<div className={styles.mosaicContainerWrapper}>
|
||||||
label = aggregateExpiry.isExpired
|
<div className={styles.mosaicContainer}>
|
||||||
? t`Expired between ${earliest} and ${latest}`
|
<div className={gridClassName}>{items.map((attachment) => renderMosaicItem(attachment))}</div>
|
||||||
: t`Expires between ${earliest} and ${latest}`;
|
</div>
|
||||||
} else if (earliest) {
|
{renderFootnote()}
|
||||||
label = aggregateExpiry.isExpired ? t`Expired on ${earliest}` : t`Expires on ${earliest}`;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AccessibilityStore.showAttachmentExpiryIndicator ? (
|
|
||||||
<ExpiryFootnote expiresAt={aggregateExpiry.expiresAt} isExpired={aggregateExpiry.isExpired} label={label} />
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (count === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderMosaicItem = (attachment: MessageAttachment, key?: string) => (
|
|
||||||
<MosaicItemBase
|
|
||||||
key={key || attachment.id}
|
|
||||||
attachment={attachment}
|
|
||||||
message={message}
|
|
||||||
mediaAttachments={mediaAttachments}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderGrid = (items: ReadonlyArray<MessageAttachment>, gridClassName: string) => (
|
|
||||||
<div className={styles.mosaicContainerWrapper}>
|
|
||||||
<div className={styles.mosaicContainer}>
|
|
||||||
<div className={gridClassName}>{items.map((attachment) => renderMosaicItem(attachment))}</div>
|
|
||||||
</div>
|
</div>
|
||||||
{renderFootnote()}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (count) {
|
switch (count) {
|
||||||
case 1:
|
case 1:
|
||||||
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
|
||||||
</div>
|
attachment={mediaAttachments[0]}
|
||||||
{renderFootnote()}
|
message={message}
|
||||||
</div>
|
mediaAttachments={mediaAttachments}
|
||||||
);
|
isPreview={isPreview}
|
||||||
|
/>
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<div className={styles.mosaicContainerWrapper}>
|
|
||||||
<div className={styles.mosaicContainer}>
|
|
||||||
<div className={styles.oneByTwoGrid}>
|
|
||||||
{mediaAttachments.map((attachment) => (
|
|
||||||
<div key={attachment.id} className={styles.oneByTwoGridItem}>
|
|
||||||
{renderMosaicItem(attachment)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
{renderFootnote()}
|
||||||
</div>
|
</div>
|
||||||
{renderFootnote()}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 3:
|
case 2:
|
||||||
return (
|
return (
|
||||||
<div className={styles.mosaicContainerWrapper}>
|
<div className={styles.mosaicContainerWrapper}>
|
||||||
<div className={styles.mosaicContainer}>
|
<div className={styles.mosaicContainer}>
|
||||||
<div className={clsx(styles.oneByTwoGrid, styles.oneByTwoLayoutThreeGrid)}>
|
<div className={styles.oneByTwoGrid}>
|
||||||
<div className={styles.oneByTwoSoloItem}>{renderMosaicItem(mediaAttachments[0])}</div>
|
{mediaAttachments.map((attachment) => (
|
||||||
<div className={styles.oneByTwoDuoItem}>
|
<div key={attachment.id} className={styles.oneByTwoGridItem}>
|
||||||
<div className={styles.twoByOneGrid}>
|
{renderMosaicItem(attachment)}
|
||||||
<div className={styles.twoByOneGridItem}>{renderMosaicItem(mediaAttachments[1])}</div>
|
</div>
|
||||||
<div className={styles.twoByOneGridItem}>{renderMosaicItem(mediaAttachments[2])}</div>
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{renderFootnote()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<div className={styles.mosaicContainerWrapper}>
|
||||||
|
<div className={styles.mosaicContainer}>
|
||||||
|
<div className={clsx(styles.oneByTwoGrid, styles.oneByTwoLayoutThreeGrid)}>
|
||||||
|
<div className={styles.oneByTwoSoloItem}>{renderMosaicItem(mediaAttachments[0])}</div>
|
||||||
|
<div className={styles.oneByTwoDuoItem}>
|
||||||
|
<div className={styles.twoByOneGrid}>
|
||||||
|
<div className={styles.twoByOneGridItem}>{renderMosaicItem(mediaAttachments[1])}</div>
|
||||||
|
<div className={styles.twoByOneGridItem}>{renderMosaicItem(mediaAttachments[2])}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{renderFootnote()}
|
||||||
</div>
|
</div>
|
||||||
{renderFootnote()}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
return renderGrid(mediaAttachments, styles.twoByTwoGrid);
|
return renderGrid(mediaAttachments, styles.twoByTwoGrid);
|
||||||
|
|
||||||
case 5:
|
case 5:
|
||||||
return (
|
return (
|
||||||
<div className={styles.mosaicContainerWrapper}>
|
<div className={styles.mosaicContainerWrapper}>
|
||||||
<div className={styles.mosaicContainer}>
|
<div className={styles.mosaicContainer}>
|
||||||
<div className={clsx(styles.fiveAttachmentContainer)}>
|
<div className={clsx(styles.fiveAttachmentContainer)}>
|
||||||
|
<div className={styles.oneByTwoGrid}>
|
||||||
|
<div className={styles.oneByTwoGridItem}>{renderMosaicItem(mediaAttachments[0])}</div>
|
||||||
|
<div className={styles.oneByTwoGridItem}>{renderMosaicItem(mediaAttachments[1])}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.threeByThreeGrid}>
|
||||||
|
{mediaAttachments.slice(2, 5).map((attachment) => renderMosaicItem(attachment))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{renderFootnote()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
return renderGrid(mediaAttachments, styles.threeByThreeGrid);
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
return (
|
||||||
|
<div className={styles.mosaicContainerWrapper}>
|
||||||
|
<div className={styles.mosaicContainer}>
|
||||||
|
<div className={clsx(styles.oneByOneGrid, styles.oneByOneGridMosaic)}>
|
||||||
|
{renderMosaicItem(mediaAttachments[0])}
|
||||||
|
</div>
|
||||||
|
<div className={styles.threeByThreeGrid}>
|
||||||
|
{mediaAttachments.slice(1, 7).map((attachment) => renderMosaicItem(attachment))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{renderFootnote()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 8:
|
||||||
|
return (
|
||||||
|
<div className={styles.mosaicContainerWrapper}>
|
||||||
|
<div className={styles.mosaicContainer}>
|
||||||
<div className={styles.oneByTwoGrid}>
|
<div className={styles.oneByTwoGrid}>
|
||||||
<div className={styles.oneByTwoGridItem}>{renderMosaicItem(mediaAttachments[0])}</div>
|
<div className={styles.oneByTwoGridItem}>{renderMosaicItem(mediaAttachments[0])}</div>
|
||||||
<div className={styles.oneByTwoGridItem}>{renderMosaicItem(mediaAttachments[1])}</div>
|
<div className={styles.oneByTwoGridItem}>{renderMosaicItem(mediaAttachments[1])}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.threeByThreeGrid}>
|
<div className={styles.threeByThreeGrid}>
|
||||||
{mediaAttachments.slice(2, 5).map((attachment) => renderMosaicItem(attachment))}
|
{mediaAttachments.slice(2, 8).map((attachment) => renderMosaicItem(attachment))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{renderFootnote()}
|
||||||
</div>
|
</div>
|
||||||
{renderFootnote()}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 6:
|
case 9:
|
||||||
return renderGrid(mediaAttachments, styles.threeByThreeGrid);
|
return renderGrid(mediaAttachments, styles.threeByThreeGrid);
|
||||||
|
|
||||||
case 7:
|
case 10:
|
||||||
return (
|
return (
|
||||||
<div className={styles.mosaicContainerWrapper}>
|
<div className={styles.mosaicContainerWrapper}>
|
||||||
<div className={styles.mosaicContainer}>
|
<div className={styles.mosaicContainer}>
|
||||||
<div className={clsx(styles.oneByOneGrid, styles.oneByOneGridMosaic)}>
|
<div className={clsx(styles.oneByOneGrid, styles.oneByOneGridMosaic)}>
|
||||||
{renderMosaicItem(mediaAttachments[0])}
|
{renderMosaicItem(mediaAttachments[0])}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.threeByThreeGrid}>
|
<div className={styles.threeByThreeGrid}>
|
||||||
{mediaAttachments.slice(1, 7).map((attachment) => renderMosaicItem(attachment))}
|
{mediaAttachments.slice(1, 10).map((attachment) => renderMosaicItem(attachment))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{renderFootnote()}
|
||||||
</div>
|
</div>
|
||||||
{renderFootnote()}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 8:
|
default:
|
||||||
return (
|
throw new Error('This should never happen');
|
||||||
<div className={styles.mosaicContainerWrapper}>
|
}
|
||||||
<div className={styles.mosaicContainer}>
|
},
|
||||||
<div className={styles.oneByTwoGrid}>
|
);
|
||||||
<div className={styles.oneByTwoGridItem}>{renderMosaicItem(mediaAttachments[0])}</div>
|
|
||||||
<div className={styles.oneByTwoGridItem}>{renderMosaicItem(mediaAttachments[1])}</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.threeByThreeGrid}>
|
|
||||||
{mediaAttachments.slice(2, 8).map((attachment) => renderMosaicItem(attachment))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{renderFootnote()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 9:
|
|
||||||
return renderGrid(mediaAttachments, styles.threeByThreeGrid);
|
|
||||||
|
|
||||||
case 10:
|
|
||||||
return (
|
|
||||||
<div className={styles.mosaicContainerWrapper}>
|
|
||||||
<div className={styles.mosaicContainer}>
|
|
||||||
<div className={clsx(styles.oneByOneGrid, styles.oneByOneGridMosaic)}>
|
|
||||||
{renderMosaicItem(mediaAttachments[0])}
|
|
||||||
</div>
|
|
||||||
<div className={styles.threeByThreeGrid}>
|
|
||||||
{mediaAttachments.slice(1, 10).map((attachment) => renderMosaicItem(attachment))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{renderFootnote()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error('This should never happen');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AttachmentMosaic: FC<AttachmentMosaicProps> = AttachmentMosaicComponent;
|
export const AttachmentMosaic: FC<AttachmentMosaicProps> = AttachmentMosaicComponent;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user