Show attachment download progress, new stop button to cancel
Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
parent
025841e5bb
commit
2741fbb5d2
78 changed files with 2192 additions and 562 deletions
|
@ -23,10 +23,8 @@ export type PropsType = {
|
|||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
platform: string;
|
||||
kickOffAttachmentDownload: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
}) => void;
|
||||
kickOffAttachmentDownload: (options: { messageId: string }) => void;
|
||||
cancelAttachmentDownload: (options: { messageId: string }) => void;
|
||||
showLightbox: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
|
@ -73,6 +71,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
};
|
||||
|
||||
export function EditHistoryMessagesModal({
|
||||
cancelAttachmentDownload,
|
||||
closeEditHistoryModal,
|
||||
getPreferredBadge,
|
||||
editHistoryMessages,
|
||||
|
@ -127,12 +126,8 @@ export function EditHistoryMessagesModal({
|
|||
isEditedMessage
|
||||
isSpoilerExpanded={revealedSpoilersById[currentMessageId] || {}}
|
||||
key={currentMessage.timestamp}
|
||||
kickOffAttachmentDownload={({ attachment }) =>
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: currentMessage.id,
|
||||
})
|
||||
}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
messageExpanded={(messageId, displayLimit) => {
|
||||
const update = {
|
||||
...displayLimitById,
|
||||
|
@ -195,12 +190,8 @@ export function EditHistoryMessagesModal({
|
|||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
isSpoilerExpanded={revealedSpoilersById[syntheticId] || {}}
|
||||
kickOffAttachmentDownload={({ attachment }) =>
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: messageAttributes.id,
|
||||
})
|
||||
}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
messageExpanded={(messageId, displayLimit) => {
|
||||
const update = {
|
||||
...displayLimitById,
|
||||
|
|
|
@ -55,6 +55,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
onToggleSelect: shouldNeverBeCalled,
|
||||
onReplyToMessage: shouldNeverBeCalled,
|
||||
kickOffAttachmentDownload: shouldNeverBeCalled,
|
||||
cancelAttachmentDownload: shouldNeverBeCalled,
|
||||
markAttachmentAsCorrupted: shouldNeverBeCalled,
|
||||
messageExpanded: shouldNeverBeCalled,
|
||||
openGiftBadge: shouldNeverBeCalled,
|
||||
|
|
91
ts/components/conversation/AttachmentDetailPill.stories.tsx
Normal file
91
ts/components/conversation/AttachmentDetailPill.stories.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { type PropsType, AttachmentDetailPill } from './AttachmentDetailPill';
|
||||
import { type ComponentMeta } from '../../storybook/types';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/AttachmentDetailPill',
|
||||
component: AttachmentDetailPill,
|
||||
argTypes: {
|
||||
isGif: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
attachments: [],
|
||||
isGif: false,
|
||||
startDownload: action('startDownload'),
|
||||
cancelDownload: action('cancelDownload'),
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
export function NoneDefaultsBlank(args: PropsType): JSX.Element {
|
||||
return <AttachmentDetailPill {...args} />;
|
||||
}
|
||||
|
||||
export function OneDownloadedBlank(args: PropsType): JSX.Element {
|
||||
return <AttachmentDetailPill {...args} attachments={[fakeAttachment()]} />;
|
||||
}
|
||||
|
||||
export function OneNotPendingNotDownloaded(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AttachmentDetailPill
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
path: undefined,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OnePendingNotDownloading(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AttachmentDetailPill
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
pending: true,
|
||||
path: undefined,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OneDownloading(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AttachmentDetailPill
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
pending: true,
|
||||
path: undefined,
|
||||
totalDownloaded: 5000,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OneNotPendingSomeDownloaded(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AttachmentDetailPill
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
path: undefined,
|
||||
totalDownloaded: 5000,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
57
ts/components/conversation/AttachmentDetailPill.tsx
Normal file
57
ts/components/conversation/AttachmentDetailPill.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { formatFileSize } from '../../util/formatFileSize';
|
||||
|
||||
import type { AttachmentForUIType } from '../../types/Attachment';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
|
||||
export type PropsType = {
|
||||
attachments: ReadonlyArray<AttachmentForUIType>;
|
||||
i18n: LocalizerType;
|
||||
isGif?: boolean;
|
||||
startDownload: () => void;
|
||||
cancelDownload: () => void;
|
||||
};
|
||||
|
||||
export function AttachmentDetailPill({
|
||||
attachments,
|
||||
isGif,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const areAllDownloaded = attachments.every(attachment => attachment.path);
|
||||
const totalSize = attachments.reduce(
|
||||
(total: number, attachment: AttachmentForUIType) => {
|
||||
return total + (attachment.size ?? 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
if (areAllDownloaded || totalSize === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalDownloadedSize = attachments.reduce(
|
||||
(total: number, attachment: AttachmentForUIType) => {
|
||||
return (
|
||||
total +
|
||||
(attachment.path ? attachment.size : (attachment.totalDownloaded ?? 0))
|
||||
);
|
||||
},
|
||||
0
|
||||
);
|
||||
const areAnyPending = attachments.some(attachment => attachment.pending);
|
||||
|
||||
return (
|
||||
<div className="AttachmentDetailPill">
|
||||
<div className="AttachmentDetailPill__text-wrapper">
|
||||
{totalDownloadedSize > 0 && areAnyPending
|
||||
? `${formatFileSize(totalDownloadedSize, 2)} / `
|
||||
: undefined}
|
||||
{formatFileSize(totalSize, 2)}
|
||||
{isGif ? ' · GIF' : undefined}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -91,7 +91,6 @@ export function AttachmentList<T extends AttachmentType | AttachmentDraftType>({
|
|||
isVideo ||
|
||||
attachment.pending
|
||||
) {
|
||||
const isDownloaded = !attachment.pending;
|
||||
const imageUrl =
|
||||
url || (isVideo ? BLANK_VIDEO_THUMBNAIL : undefined);
|
||||
|
||||
|
@ -108,7 +107,6 @@ export function AttachmentList<T extends AttachmentType | AttachmentDraftType>({
|
|||
className="module-staged-attachment"
|
||||
i18n={i18n}
|
||||
attachment={attachment}
|
||||
isDownloaded={isDownloaded}
|
||||
curveBottomLeft={CurveType.Tiny}
|
||||
curveBottomRight={CurveType.Tiny}
|
||||
curveTopLeft={CurveType.Tiny}
|
||||
|
@ -118,7 +116,7 @@ export function AttachmentList<T extends AttachmentType | AttachmentDraftType>({
|
|||
width={IMAGE_WIDTH}
|
||||
url={imageUrl}
|
||||
closeButton
|
||||
onClick={clickAttachment}
|
||||
showVisualAttachment={clickAttachment}
|
||||
onClickClose={closeAttachment}
|
||||
onError={closeAttachment}
|
||||
/>
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import type { AttachmentForUIType } from '../../types/Attachment';
|
||||
import {
|
||||
hasNotResolved,
|
||||
getImageDimensions,
|
||||
|
@ -17,21 +16,26 @@ import {
|
|||
import * as Errors from '../../types/errors';
|
||||
import * as log from '../../logging/log';
|
||||
import { useReducedMotion } from '../../hooks/useReducedMotion';
|
||||
import { AttachmentDetailPill } from './AttachmentDetailPill';
|
||||
import { getSpinner } from './Image';
|
||||
|
||||
const MAX_GIF_REPEAT = 4;
|
||||
const MAX_GIF_TIME = 8;
|
||||
|
||||
export type Props = {
|
||||
readonly attachment: AttachmentType;
|
||||
readonly attachment: AttachmentForUIType;
|
||||
readonly size?: number;
|
||||
readonly tabIndex: number;
|
||||
// test-only, to force reduced motion experience
|
||||
readonly _forceTapToPlay?: boolean;
|
||||
|
||||
readonly i18n: LocalizerType;
|
||||
readonly theme?: ThemeType;
|
||||
|
||||
onError(): void;
|
||||
showVisualAttachment(): void;
|
||||
kickOffAttachmentDownload(): void;
|
||||
startDownload(): void;
|
||||
cancelDownload(): void;
|
||||
};
|
||||
|
||||
type MediaEvent = React.SyntheticEvent<HTMLVideoElement, Event>;
|
||||
|
@ -41,16 +45,18 @@ export function GIF(props: Props): JSX.Element {
|
|||
attachment,
|
||||
size,
|
||||
tabIndex,
|
||||
_forceTapToPlay,
|
||||
|
||||
i18n,
|
||||
theme,
|
||||
|
||||
onError,
|
||||
showVisualAttachment,
|
||||
kickOffAttachmentDownload,
|
||||
startDownload,
|
||||
cancelDownload,
|
||||
} = props;
|
||||
|
||||
const tapToPlay = useReducedMotion();
|
||||
const tapToPlay = useReducedMotion() || _forceTapToPlay;
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const { height, width } = getImageDimensions(attachment, size);
|
||||
|
@ -142,7 +148,7 @@ export function GIF(props: Props): JSX.Element {
|
|||
event.stopPropagation();
|
||||
|
||||
if (!attachment.url) {
|
||||
kickOffAttachmentDownload();
|
||||
startDownload();
|
||||
} else if (tapToPlay) {
|
||||
setPlayTime(0);
|
||||
setCurrentTime(0);
|
||||
|
@ -158,21 +164,18 @@ export function GIF(props: Props): JSX.Element {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
kickOffAttachmentDownload();
|
||||
if (!attachment.url) {
|
||||
startDownload();
|
||||
} else if (tapToPlay) {
|
||||
setPlayTime(0);
|
||||
setCurrentTime(0);
|
||||
setRepeatCount(0);
|
||||
}
|
||||
};
|
||||
|
||||
const isPending = Boolean(attachment.pending);
|
||||
const isNotResolved = hasNotResolved(attachment) && !isPending;
|
||||
|
||||
let fileSize: JSX.Element | undefined;
|
||||
if (isNotResolved && attachment.fileSize) {
|
||||
fileSize = (
|
||||
<div className="module-image--gif__filesize">
|
||||
{attachment.fileSize} · GIF
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let gif: JSX.Element | undefined;
|
||||
if (isNotResolved || isPending) {
|
||||
gif = (
|
||||
|
@ -208,6 +211,35 @@ export function GIF(props: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
const cancelDownloadClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (cancelDownload) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelDownload();
|
||||
}
|
||||
},
|
||||
[cancelDownload]
|
||||
);
|
||||
const cancelDownloadKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (cancelDownload && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelDownload();
|
||||
}
|
||||
},
|
||||
[cancelDownload]
|
||||
);
|
||||
|
||||
const spinner = getSpinner({
|
||||
attachment,
|
||||
i18n,
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
let overlay: JSX.Element | undefined;
|
||||
if ((tapToPlay && !isPlaying) || isNotResolved) {
|
||||
const className = classNames([
|
||||
|
@ -232,26 +264,22 @@ export function GIF(props: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
let spinner: JSX.Element | undefined;
|
||||
if (isPending) {
|
||||
spinner = (
|
||||
<div className="module-image__download-pending--spinner-container">
|
||||
<div
|
||||
className="module-image__download-pending--spinner"
|
||||
title={i18n('icu:loading')}
|
||||
>
|
||||
<Spinner moduleClassName="module-image-spinner" svgSize="small" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const detailPill = (
|
||||
<AttachmentDetailPill
|
||||
attachments={[attachment]}
|
||||
cancelDownload={cancelDownload}
|
||||
i18n={i18n}
|
||||
isGif
|
||||
startDownload={startDownload}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="module-image module-image--gif">
|
||||
{gif}
|
||||
{overlay}
|
||||
{spinner}
|
||||
{fileSize}
|
||||
{overlay}
|
||||
{detailPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,11 +38,13 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
curveTopLeft: overrideProps.curveTopLeft || CurveType.None,
|
||||
curveTopRight: overrideProps.curveTopRight || CurveType.None,
|
||||
darkOverlay: overrideProps.darkOverlay || false,
|
||||
height: overrideProps.height || 100,
|
||||
height: overrideProps.height || 200,
|
||||
i18n,
|
||||
noBackground: overrideProps.noBackground || false,
|
||||
noBorder: overrideProps.noBorder || false,
|
||||
onClick: action('onClick'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
startDownload: action('startDownload'),
|
||||
cancelDownload: action('cancelDownload'),
|
||||
onClickClose: action('onClickClose'),
|
||||
onError: action('onError'),
|
||||
overlayText: overrideProps.overlayText || '',
|
||||
|
@ -50,7 +52,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
tabIndex: overrideProps.tabIndex || 0,
|
||||
theme: overrideProps.theme || ('light' as ThemeType),
|
||||
url: 'url' in overrideProps ? overrideProps.url || '' : pngUrl,
|
||||
width: overrideProps.width || 100,
|
||||
width: overrideProps.width || 300,
|
||||
});
|
||||
|
||||
export function UrlWithHeightWidth(): JSX.Element {
|
||||
|
@ -107,37 +109,68 @@ export function NoBorderOrBackground(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function Pending(): JSX.Element {
|
||||
export function NotDownloadedNotIncrementalNotPending(): JSX.Element {
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
url: pngUrl,
|
||||
pending: true,
|
||||
path: undefined,
|
||||
size: 5300000,
|
||||
}),
|
||||
url: undefined,
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
||||
export function PendingWBlurhash(): JSX.Element {
|
||||
export function PendingWDownloadQueuedNotIncremental(): JSX.Element {
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
url: pngUrl,
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 5300000,
|
||||
}),
|
||||
url: undefined,
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
});
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...props}
|
||||
blurHash="LDA,FDBnm+I=p{tkIUI;~UkpELV]"
|
||||
width={300}
|
||||
height={400}
|
||||
/>
|
||||
);
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
||||
export function PendingWDownloadProgress(): JSX.Element {
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 5300000,
|
||||
totalDownloaded: 1230000,
|
||||
}),
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
||||
export function NotPendingWDownloadProgress(): JSX.Element {
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
size: 5300000,
|
||||
totalDownloaded: 1230000,
|
||||
}),
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
||||
export function CurvedCorners(): JSX.Element {
|
||||
|
@ -188,11 +221,14 @@ export function FullOverlayWithText(): JSX.Element {
|
|||
}
|
||||
|
||||
export function Blurhash(): JSX.Element {
|
||||
const defaultProps = createProps();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
}),
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
};
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
@ -213,12 +249,10 @@ export function UndefinedBlurHash(): JSX.Element {
|
|||
}
|
||||
|
||||
export function MissingImage(): JSX.Element {
|
||||
const defaultProps = createProps();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
attachment: undefined as any,
|
||||
};
|
||||
const props = createProps({
|
||||
attachment: undefined,
|
||||
url: 'random',
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
|
|
@ -2,17 +2,18 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import { Spinner } from '../Spinner';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import {
|
||||
isDownloaded as isDownloadedFunction,
|
||||
defaultBlurHash,
|
||||
import type {
|
||||
AttachmentForUIType,
|
||||
AttachmentType,
|
||||
} from '../../types/Attachment';
|
||||
import { defaultBlurHash, isReadyToView } from '../../types/Attachment';
|
||||
import { ProgressCircle } from '../ProgressCircle';
|
||||
|
||||
export enum CurveType {
|
||||
None = 0,
|
||||
|
@ -23,10 +24,9 @@ export enum CurveType {
|
|||
|
||||
export type Props = {
|
||||
alt: string;
|
||||
attachment: AttachmentType;
|
||||
attachment: AttachmentForUIType;
|
||||
url?: string;
|
||||
|
||||
isDownloaded?: boolean;
|
||||
className?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
|
@ -51,7 +51,9 @@ export type Props = {
|
|||
|
||||
i18n: LocalizerType;
|
||||
theme?: ThemeType;
|
||||
onClick?: (attachment: AttachmentType) => void;
|
||||
showVisualAttachment?: (attachment: AttachmentType) => void;
|
||||
cancelDownload?: () => void;
|
||||
startDownload?: () => void;
|
||||
onClickClose?: (attachment: AttachmentType) => void;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
@ -68,12 +70,13 @@ export function Image({
|
|||
curveTopLeft,
|
||||
curveTopRight,
|
||||
darkOverlay,
|
||||
isDownloaded,
|
||||
height = 0,
|
||||
i18n,
|
||||
noBackground,
|
||||
noBorder,
|
||||
onClick,
|
||||
showVisualAttachment,
|
||||
startDownload,
|
||||
cancelDownload,
|
||||
onClickClose,
|
||||
onError,
|
||||
overlayText,
|
||||
|
@ -85,11 +88,6 @@ export function Image({
|
|||
cropWidth = 0,
|
||||
cropHeight = 0,
|
||||
}: Props): JSX.Element {
|
||||
const { caption, pending } = attachment || { caption: null, pending: true };
|
||||
const imgNotDownloaded = isDownloaded
|
||||
? false
|
||||
: !isDownloadedFunction(attachment);
|
||||
|
||||
const resolvedBlurHash = blurHash || defaultBlurHash(theme);
|
||||
|
||||
const curveStyles: CSSProperties = {
|
||||
|
@ -99,48 +97,112 @@ export function Image({
|
|||
borderEndEndRadius: curveBottomRight || CurveType.None,
|
||||
};
|
||||
|
||||
const canClick = useMemo(() => {
|
||||
return onClick != null && !pending;
|
||||
}, [pending, onClick]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
const showVisualAttachmentClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (!canClick) {
|
||||
if (showVisualAttachment) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClick(attachment);
|
||||
showVisualAttachment(attachment);
|
||||
}
|
||||
},
|
||||
[attachment, canClick, onClick]
|
||||
[attachment, showVisualAttachment]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
const showVisualAttachmentKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (!canClick) {
|
||||
if (
|
||||
showVisualAttachment &&
|
||||
(event.key === 'Enter' || event.key === 'Space')
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (onClick && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClick(attachment);
|
||||
showVisualAttachment(attachment);
|
||||
}
|
||||
},
|
||||
[attachment, canClick, onClick]
|
||||
[attachment, showVisualAttachment]
|
||||
);
|
||||
const cancelDownloadClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (cancelDownload) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelDownload();
|
||||
}
|
||||
},
|
||||
[cancelDownload]
|
||||
);
|
||||
const cancelDownloadKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (cancelDownload && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelDownload();
|
||||
}
|
||||
},
|
||||
[cancelDownload]
|
||||
);
|
||||
const startDownloadClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (startDownload) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startDownload();
|
||||
}
|
||||
},
|
||||
[startDownload]
|
||||
);
|
||||
const startDownloadKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (startDownload && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startDownload();
|
||||
}
|
||||
},
|
||||
[startDownload]
|
||||
);
|
||||
|
||||
/* eslint-disable no-nested-ternary */
|
||||
const imageOrBlurHash = url ? (
|
||||
<img
|
||||
onError={onError}
|
||||
className="module-image__image"
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={width}
|
||||
src={url}
|
||||
/>
|
||||
) : (
|
||||
<Blurhash
|
||||
hash={resolvedBlurHash}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
);
|
||||
|
||||
const startDownloadButton =
|
||||
startDownload && !attachment.path && !attachment.pending ? (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image__overlay-circle"
|
||||
aria-label={i18n('icu:startDownload')}
|
||||
onClick={startDownloadClick}
|
||||
onKeyDown={startDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__download-icon" />
|
||||
</button>
|
||||
) : undefined;
|
||||
|
||||
const spinner = !cancelDownload
|
||||
? undefined
|
||||
: getSpinner({
|
||||
attachment,
|
||||
i18n,
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -155,70 +217,11 @@ export function Image({
|
|||
...curveStyles,
|
||||
}}
|
||||
>
|
||||
{pending ? (
|
||||
url || blurHash ? (
|
||||
<div className="module-image__download-pending">
|
||||
{url ? (
|
||||
<img
|
||||
onError={onError}
|
||||
className="module-image__image"
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={width}
|
||||
src={url}
|
||||
/>
|
||||
) : blurHash ? (
|
||||
<Blurhash
|
||||
hash={blurHash}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
) : undefined}
|
||||
<div className="module-image__download-pending--spinner-container">
|
||||
<div
|
||||
className="module-image__download-pending--spinner"
|
||||
title={i18n('icu:loading')}
|
||||
>
|
||||
<Spinner
|
||||
moduleClassName="module-image-spinner"
|
||||
svgSize="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="module-image__loading-placeholder"
|
||||
style={{
|
||||
height: `${height}px`,
|
||||
width: `${width}px`,
|
||||
lineHeight: `${height}px`,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
title={i18n('icu:loading')}
|
||||
>
|
||||
<Spinner svgSize="normal" />
|
||||
</div>
|
||||
)
|
||||
) : url ? (
|
||||
<img
|
||||
onError={onError}
|
||||
className="module-image__image"
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={width}
|
||||
src={url}
|
||||
/>
|
||||
) : resolvedBlurHash ? (
|
||||
<Blurhash
|
||||
hash={resolvedBlurHash}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
) : null}
|
||||
{caption ? (
|
||||
{imageOrBlurHash}
|
||||
{startDownloadButton}
|
||||
{spinner}
|
||||
|
||||
{attachment.caption ? (
|
||||
<img
|
||||
className="module-image__caption-icon"
|
||||
src="images/caption-shadow.svg"
|
||||
|
@ -234,9 +237,9 @@ export function Image({
|
|||
}}
|
||||
/>
|
||||
) : null}
|
||||
{!pending && !imgNotDownloaded && playIconOverlay ? (
|
||||
<div className="module-image__play-overlay__circle">
|
||||
<div className="module-image__play-overlay__icon" />
|
||||
{attachment.path && playIconOverlay ? (
|
||||
<div className="module-image__overlay-circle">
|
||||
<div className="module-image__play-icon" />
|
||||
</div>
|
||||
) : null}
|
||||
{overlayText ? (
|
||||
|
@ -247,22 +250,27 @@ export function Image({
|
|||
{overlayText}
|
||||
</div>
|
||||
) : null}
|
||||
{canClick ? (
|
||||
{darkOverlay || !noBorder ? (
|
||||
<div
|
||||
className={classNames('module-image__border-overlay', {
|
||||
'module-image__border-overlay--with-border': !noBorder,
|
||||
'module-image__border-overlay--dark': darkOverlay,
|
||||
})}
|
||||
style={curveStyles}
|
||||
/>
|
||||
) : null}
|
||||
{showVisualAttachment && isReadyToView(attachment) ? (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames('module-image__border-overlay', {
|
||||
'module-image__border-overlay--with-border': !noBorder,
|
||||
'module-image__border-overlay--with-click-handler': canClick,
|
||||
'module-image__border-overlay--dark': darkOverlay,
|
||||
'module-image--not-downloaded': imgNotDownloaded,
|
||||
'module-image__border-overlay--with-click-handler': true,
|
||||
})}
|
||||
aria-label={i18n('icu:imageOpenAlt')}
|
||||
style={curveStyles}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={showVisualAttachmentClick}
|
||||
onKeyDown={showVisualAttachmentKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{imgNotDownloaded ? <span /> : null}
|
||||
</button>
|
||||
/>
|
||||
) : null}
|
||||
{closeButton ? (
|
||||
<button
|
||||
|
@ -282,5 +290,71 @@ export function Image({
|
|||
) : null}
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable no-nested-ternary */
|
||||
}
|
||||
|
||||
export function getSpinner({
|
||||
attachment,
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
i18n,
|
||||
tabIndex,
|
||||
}: {
|
||||
attachment: AttachmentForUIType;
|
||||
cancelDownloadClick: (event: React.MouseEvent) => void;
|
||||
cancelDownloadKeyDown: (
|
||||
event: React.KeyboardEvent<HTMLButtonElement>
|
||||
) => void;
|
||||
i18n: LocalizerType;
|
||||
tabIndex: number | undefined;
|
||||
}): JSX.Element | undefined {
|
||||
const downloadFraction =
|
||||
attachment.pending && attachment.size && attachment.totalDownloaded
|
||||
? attachment.totalDownloaded / attachment.size
|
||||
: undefined;
|
||||
|
||||
if (downloadFraction) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image__overlay-circle"
|
||||
aria-label={i18n('icu:cancelDownload')}
|
||||
onClick={cancelDownloadClick}
|
||||
onKeyDown={cancelDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__stop-icon" />
|
||||
<div className="module-image__progress-circle-wrapper">
|
||||
<ProgressCircle
|
||||
fractionComplete={downloadFraction}
|
||||
width={44}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!attachment.pending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image__overlay-circle"
|
||||
aria-label={i18n('icu:cancelDownload')}
|
||||
onClick={cancelDownloadClick}
|
||||
onKeyDown={cancelDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__spinner-container">
|
||||
<Spinner
|
||||
moduleClassName="module-image-spinner"
|
||||
svgSize="normal"
|
||||
size="44px"
|
||||
/>
|
||||
<div className="module-image__stop-icon" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ export default {
|
|||
direction: 'incoming',
|
||||
i18n,
|
||||
isSticker: false,
|
||||
onClick: action('onClick'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
startDownload: action('startDownload'),
|
||||
cancelDownload: action('cancelDownload'),
|
||||
onError: action('onError'),
|
||||
stickerSize: 0,
|
||||
tabIndex: 0,
|
||||
|
@ -57,13 +59,111 @@ export function OneImage(args: Props): JSX.Element {
|
|||
return <ImageGrid {...args} />;
|
||||
}
|
||||
|
||||
export function OneVideo(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
url: pngUrl,
|
||||
width: 800,
|
||||
screenshot: {
|
||||
path: 'something',
|
||||
url: pngUrl,
|
||||
contentType: IMAGE_PNG,
|
||||
height: 1200,
|
||||
width: 800,
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function OneVideoNotDownloadedNotPending(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function OneVideoPendingWDownloadQueued(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
url: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function OneVideoPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function OneVideoDownloadProgressNotPending(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
export function TwoImages(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
url: pngUrl,
|
||||
|
@ -81,6 +181,62 @@ export function TwoImages(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function TwoImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TwoImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function ThreeImages(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -112,6 +268,74 @@ export function ThreeImages(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function ThreeImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function ThreeImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FourImages(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -150,6 +374,89 @@ export function FourImages(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function FourImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function FourImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FiveImages(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -195,6 +502,104 @@ export function FiveImages(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function FiveImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function FiveImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const _6Images = (args: Props): JSX.Element => {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -254,6 +659,63 @@ export const _6Images = (args: Props): JSX.Element => {
|
|||
);
|
||||
};
|
||||
|
||||
export function _6ImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
export function MixedContentTypes(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -295,6 +757,80 @@ export function MixedContentTypes(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function EightImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sticker(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { Image, CurveType } from './Image';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import { AttachmentDetailPill } from './AttachmentDetailPill';
|
||||
|
||||
export type DirectionType = 'incoming' | 'outgoing';
|
||||
|
||||
|
@ -39,7 +40,9 @@ export type Props = {
|
|||
theme?: ThemeType;
|
||||
|
||||
onError: () => void;
|
||||
onClick?: (attachment: AttachmentType) => void;
|
||||
showVisualAttachment: (attachment: AttachmentType) => void;
|
||||
cancelDownload: () => void;
|
||||
startDownload: () => void;
|
||||
};
|
||||
|
||||
const GAP = 1;
|
||||
|
@ -108,7 +111,9 @@ export function ImageGrid({
|
|||
isSticker,
|
||||
stickerSize,
|
||||
onError,
|
||||
onClick,
|
||||
showVisualAttachment,
|
||||
cancelDownload,
|
||||
startDownload,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
tabIndex,
|
||||
|
@ -127,10 +132,46 @@ export function ImageGrid({
|
|||
|
||||
const withBottomOverlay = Boolean(bottomOverlay && !withContentBelow);
|
||||
|
||||
const startDownloadClick = React.useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (startDownload) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startDownload();
|
||||
}
|
||||
},
|
||||
[startDownload]
|
||||
);
|
||||
const startDownloadKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (startDownload && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startDownload();
|
||||
}
|
||||
},
|
||||
[startDownload]
|
||||
);
|
||||
|
||||
if (!attachments || !attachments.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const detailPill = (
|
||||
<AttachmentDetailPill
|
||||
attachments={attachments}
|
||||
i18n={i18n}
|
||||
startDownload={startDownload}
|
||||
cancelDownload={cancelDownload}
|
||||
/>
|
||||
);
|
||||
const downloadPill = renderDownloadPill({
|
||||
attachments,
|
||||
i18n,
|
||||
startDownloadClick,
|
||||
startDownloadKeyDown,
|
||||
});
|
||||
|
||||
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
|
||||
const { height, width } = getImageDimensions(
|
||||
attachments[0],
|
||||
|
@ -165,9 +206,12 @@ export function ImageGrid({
|
|||
getUrl(attachments[0]) ?? attachments[0].thumbnailFromBackup?.url
|
||||
}
|
||||
tabIndex={tabIndex}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
{detailPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -190,7 +234,9 @@ export function ImageGrid({
|
|||
width={150}
|
||||
cropWidth={GAP}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -207,9 +253,13 @@ export function ImageGrid({
|
|||
width={150}
|
||||
attachment={attachments[1]}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
{detailPill}
|
||||
{downloadPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -232,7 +282,9 @@ export function ImageGrid({
|
|||
width={200}
|
||||
cropWidth={GAP}
|
||||
url={getUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<div className="module-image-grid__column">
|
||||
|
@ -248,7 +300,9 @@ export function ImageGrid({
|
|||
attachment={attachments[1]}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -264,10 +318,14 @@ export function ImageGrid({
|
|||
attachment={attachments[2]}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
{detailPill}
|
||||
{downloadPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -291,7 +349,9 @@ export function ImageGrid({
|
|||
cropHeight={GAP}
|
||||
cropWidth={GAP}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -307,7 +367,9 @@ export function ImageGrid({
|
|||
cropHeight={GAP}
|
||||
attachment={attachments[1]}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
|
@ -326,7 +388,9 @@ export function ImageGrid({
|
|||
cropWidth={GAP}
|
||||
attachment={attachments[2]}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -342,11 +406,15 @@ export function ImageGrid({
|
|||
width={150}
|
||||
attachment={attachments[3]}
|
||||
url={getThumbnailUrl(attachments[3])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{detailPill}
|
||||
{downloadPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -372,7 +440,9 @@ export function ImageGrid({
|
|||
width={150}
|
||||
cropWidth={GAP}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -386,7 +456,9 @@ export function ImageGrid({
|
|||
width={150}
|
||||
attachment={attachments[1]}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
|
@ -405,7 +477,9 @@ export function ImageGrid({
|
|||
cropWidth={GAP}
|
||||
attachment={attachments[2]}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -421,7 +495,9 @@ export function ImageGrid({
|
|||
cropWidth={GAP}
|
||||
attachment={attachments[3]}
|
||||
url={getThumbnailUrl(attachments[3])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -439,11 +515,51 @@ export function ImageGrid({
|
|||
overlayText={moreMessagesOverlayText}
|
||||
attachment={attachments[4]}
|
||||
url={getThumbnailUrl(attachments[4])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={undefined}
|
||||
startDownload={undefined}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{detailPill}
|
||||
{downloadPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDownloadPill({
|
||||
attachments,
|
||||
i18n,
|
||||
startDownloadClick,
|
||||
startDownloadKeyDown,
|
||||
}: {
|
||||
attachments: ReadonlyArray<AttachmentForUIType>;
|
||||
i18n: LocalizerType;
|
||||
startDownloadClick: (event: React.MouseEvent) => void;
|
||||
startDownloadKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
|
||||
}): JSX.Element | null {
|
||||
const downloadedOrPending = attachments.some(
|
||||
attachment => attachment.path || attachment.pending
|
||||
);
|
||||
if (downloadedOrPending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image-grid__download-pill"
|
||||
aria-label={i18n('icu:startDownload')}
|
||||
onClick={startDownloadClick}
|
||||
onKeyDown={startDownloadKeyDown}
|
||||
>
|
||||
<div className="module-image-grid__download_pill__icon-wrapper">
|
||||
<div className="module-image-grid__download_pill__download-icon" />
|
||||
</div>
|
||||
<div className="module-image-grid__download_pill__text-wrapper">
|
||||
{i18n('icu:downloadNItems', { count: attachments.length })}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import React from 'react';
|
|||
import { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import getDirection from 'direction';
|
||||
import { drop, groupBy, noop, orderBy, take, unescape } from 'lodash';
|
||||
import { drop, groupBy, orderBy, take, unescape } from 'lodash';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
@ -52,7 +52,10 @@ import type { WidthBreakpoint } from '../_util';
|
|||
import { OutgoingGiftBadgeModal } from '../OutgoingGiftBadgeModal';
|
||||
import * as log from '../../logging/log';
|
||||
import { StoryViewModeType } from '../../types/Stories';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import type {
|
||||
AttachmentForUIType,
|
||||
AttachmentType,
|
||||
} from '../../types/Attachment';
|
||||
import {
|
||||
canDisplayImage,
|
||||
getExtensionForDisplay,
|
||||
|
@ -101,6 +104,7 @@ import { UserText } from '../UserText';
|
|||
import { getColorForCallLink } from '../../util/getColorForCallLink';
|
||||
import { getKeyFromCallLink } from '../../util/callLinks';
|
||||
import { InAnotherCallTooltip } from './InAnotherCallTooltip';
|
||||
import { formatFileSize } from '../../util/formatFileSize';
|
||||
|
||||
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 16;
|
||||
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
|
||||
|
@ -173,7 +177,7 @@ export type AudioAttachmentProps = {
|
|||
i18n: LocalizerType;
|
||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||
theme: ThemeType | undefined;
|
||||
attachment: AttachmentType;
|
||||
attachment: AttachmentForUIType;
|
||||
collapseMetadata: boolean;
|
||||
withContentAbove: boolean;
|
||||
withContentBelow: boolean;
|
||||
|
@ -226,7 +230,7 @@ export type PropsData = {
|
|||
activeCallConversationId?: string;
|
||||
text?: string;
|
||||
textDirection: TextDirection;
|
||||
textAttachment?: AttachmentType;
|
||||
textAttachment?: AttachmentForUIType;
|
||||
isEditedMessage?: boolean;
|
||||
isSticker?: boolean;
|
||||
isTargeted?: boolean;
|
||||
|
@ -255,7 +259,7 @@ export type PropsData = {
|
|||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
conversationType: ConversationTypeType;
|
||||
attachments?: ReadonlyArray<AttachmentType>;
|
||||
attachments?: ReadonlyArray<AttachmentForUIType>;
|
||||
giftBadge?: GiftBadgeType;
|
||||
payment?: AnyPaymentEvent;
|
||||
quote?: {
|
||||
|
@ -312,6 +316,8 @@ export type PropsData = {
|
|||
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
|
||||
item?: never;
|
||||
// test-only, to force GIF's reduced motion experience
|
||||
_forceTapToPlay?: boolean;
|
||||
};
|
||||
|
||||
export type PropsHousekeeping = {
|
||||
|
@ -344,10 +350,8 @@ export type PropsActions = {
|
|||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
showSpoiler: (messageId: string, data: Record<number, boolean>) => void;
|
||||
|
||||
kickOffAttachmentDownload: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
}) => void;
|
||||
cancelAttachmentDownload: (options: { messageId: string }) => void;
|
||||
kickOffAttachmentDownload: (options: { messageId: string }) => void;
|
||||
markAttachmentAsCorrupted: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
|
@ -919,10 +923,12 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const {
|
||||
attachments,
|
||||
attachmentDroppedDueToSize,
|
||||
cancelAttachmentDownload,
|
||||
conversationId,
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
_forceTapToPlay,
|
||||
i18n,
|
||||
id,
|
||||
isSticker,
|
||||
|
@ -978,9 +984,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
<GIF
|
||||
attachment={firstAttachment}
|
||||
size={GIF_SIZE}
|
||||
tabIndex={0}
|
||||
_forceTapToPlay={_forceTapToPlay}
|
||||
theme={theme}
|
||||
i18n={i18n}
|
||||
tabIndex={0}
|
||||
onError={this.handleImageError}
|
||||
showVisualAttachment={() => {
|
||||
showLightbox({
|
||||
|
@ -988,9 +995,13 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
messageId: id,
|
||||
});
|
||||
}}
|
||||
kickOffAttachmentDownload={() => {
|
||||
startDownload={() => {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: firstAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
}}
|
||||
cancelDownload={() => {
|
||||
cancelAttachmentDownload({
|
||||
messageId: id,
|
||||
});
|
||||
}}
|
||||
|
@ -1026,12 +1037,14 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
shouldCollapseAbove={shouldCollapseAbove}
|
||||
shouldCollapseBelow={shouldCollapseBelow}
|
||||
tabIndex={tabIndex}
|
||||
onClick={attachment => {
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({ attachment, messageId: id });
|
||||
} else {
|
||||
showLightbox({ attachment, messageId: id });
|
||||
}
|
||||
showVisualAttachment={attachment => {
|
||||
showLightbox({ attachment, messageId: id });
|
||||
}}
|
||||
startDownload={() => {
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
cancelDownload={() => {
|
||||
cancelAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1063,10 +1076,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
timestamp,
|
||||
|
||||
kickOffAttachmentDownload() {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: firstAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
},
|
||||
onCorrupted() {
|
||||
markAttachmentAsCorrupted({
|
||||
|
@ -1076,7 +1086,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
},
|
||||
});
|
||||
}
|
||||
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
||||
const { pending, fileName, size, contentType } = firstAttachment;
|
||||
const extension = getExtensionForDisplay({ contentType, fileName });
|
||||
const isDangerous = isFileDangerous(fileName || '');
|
||||
|
||||
|
@ -1100,7 +1110,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
if (!isDownloaded(firstAttachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: firstAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
} else {
|
||||
|
@ -1143,7 +1152,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
`module-message__generic-attachment__file-size--${direction}`
|
||||
)}
|
||||
>
|
||||
{fileSize}
|
||||
{formatFileSize(size)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
@ -1158,6 +1167,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
i18n,
|
||||
id,
|
||||
kickOffAttachmentDownload,
|
||||
cancelAttachmentDownload,
|
||||
previews,
|
||||
quote,
|
||||
shouldCollapseAbove,
|
||||
|
@ -1209,18 +1219,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
'module-message__link-preview--nonclickable': !isClickable,
|
||||
}
|
||||
);
|
||||
const onPreviewImageClick = isClickable
|
||||
? () => {
|
||||
if (first.image && !isDownloaded(first.image)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: first.image,
|
||||
messageId: id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
openLinkInWebBrowser(first.url);
|
||||
}
|
||||
: noop;
|
||||
const contents = (
|
||||
<>
|
||||
{first.image && previewHasImage && isFullSizeImage ? (
|
||||
|
@ -1233,7 +1231,15 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
onError={this.handleImageError}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
onClick={onPreviewImageClick}
|
||||
showVisualAttachment={() => {
|
||||
openLinkInWebBrowser(first.url);
|
||||
}}
|
||||
startDownload={() => {
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
cancelDownload={() => {
|
||||
cancelAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div dir="auto" className="module-message__link-preview__content">
|
||||
|
@ -1261,7 +1267,15 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
blurHash={first.image.blurHash}
|
||||
onError={this.handleImageError}
|
||||
i18n={i18n}
|
||||
onClick={onPreviewImageClick}
|
||||
showVisualAttachment={() => {
|
||||
openLinkInWebBrowser(first.url);
|
||||
}}
|
||||
startDownload={() => {
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
cancelDownload={() => {
|
||||
cancelAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -1970,7 +1984,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return;
|
||||
}
|
||||
kickOffAttachmentDownload({
|
||||
attachment: textAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
}}
|
||||
|
@ -2574,10 +2587,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
if (attachments && !isDownloaded(attachments[0])) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: attachments[0],
|
||||
messageId: id,
|
||||
});
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -2597,9 +2607,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const attachment = attachments[0];
|
||||
|
||||
kickOffAttachmentDownload({ attachment, messageId: id });
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -2699,10 +2707,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const attachment = attachments[0];
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: id,
|
||||
});
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ export type PropsSmartActions = Pick<MessagePropsType, 'renderAudioAttachment'>;
|
|||
|
||||
export type PropsReduxActions = Pick<
|
||||
MessagePropsType,
|
||||
| 'cancelAttachmentDownload'
|
||||
| 'checkForAccount'
|
||||
| 'clearTargetedMessage'
|
||||
| 'doubleCheckMissingQuoteReference'
|
||||
|
@ -125,6 +126,7 @@ export function MessageDetail({
|
|||
message,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
cancelAttachmentDownload,
|
||||
checkForAccount,
|
||||
clearTargetedMessage,
|
||||
contactNameColor,
|
||||
|
@ -330,6 +332,7 @@ export function MessageDetail({
|
|||
<Message
|
||||
{...message}
|
||||
renderingContext="conversation/MessageDetail"
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
checkForAccount={checkForAccount}
|
||||
clearTargetedMessage={clearTargetedMessage}
|
||||
contactNameColor={contactNameColor}
|
||||
|
|
|
@ -108,6 +108,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||
isSMS: false,
|
||||
isSpoilerExpanded: {},
|
||||
toggleSelectMessage: action('toggleSelectMessage'),
|
||||
cancelAttachmentDownload: action('default--cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('default--kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
|
||||
messageExpanded: action('default--message-expanded'),
|
||||
|
|
|
@ -296,6 +296,7 @@ const actions = () => ({
|
|||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showConversation: action('showConversation'),
|
||||
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
|
|
|
@ -76,6 +76,7 @@ const getDefaultProps = () => ({
|
|||
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||
retryMessageSend: action('retryMessageSend'),
|
||||
blockGroupLinkRequests: action('blockGroupLinkRequests'),
|
||||
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
|
|
|
@ -300,6 +300,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
isTapToView: overrideProps.isTapToView,
|
||||
isTapToViewError: overrideProps.isTapToViewError,
|
||||
isTapToViewExpired: overrideProps.isTapToViewExpired,
|
||||
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
|
@ -1400,6 +1401,22 @@ Gif.args = {
|
|||
status: 'sent',
|
||||
};
|
||||
|
||||
export const GifReducedMotion = Template.bind({});
|
||||
GifReducedMotion.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
url: '/fixtures/cat-gif.mp4',
|
||||
width: 400,
|
||||
height: 332,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
_forceTapToPlay: true,
|
||||
};
|
||||
|
||||
export const GifInAGroup = Template.bind({});
|
||||
GifInAGroup.args = {
|
||||
attachments: [
|
||||
|
@ -1423,10 +1440,10 @@ NotDownloadedGif.args = {
|
|||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
fileSize: '188.61 KB',
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
width: 400,
|
||||
height: 332,
|
||||
path: undefined,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1440,10 +1457,48 @@ PendingGif.args = {
|
|||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
fileSize: '188.61 KB',
|
||||
size: 188610,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
width: 400,
|
||||
height: 332,
|
||||
path: undefined,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
export const DownloadingGif = Template.bind({});
|
||||
DownloadingGif.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
pending: true,
|
||||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
size: 188610,
|
||||
totalDownloaded: 101010,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
width: 400,
|
||||
height: 332,
|
||||
path: undefined,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
export const PartialDownloadNotPendingGif = Template.bind({});
|
||||
PartialDownloadNotPendingGif.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
size: 188610,
|
||||
totalDownloaded: 101010,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
width: 400,
|
||||
height: 332,
|
||||
path: undefined,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1553,7 +1608,6 @@ OtherFileType.args = {
|
|||
contentType: stringToMIMEType('text/plain'),
|
||||
fileName: 'my-resume.txt',
|
||||
url: 'my-resume.txt',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1566,7 +1620,6 @@ OtherFileTypeWithCaption.args = {
|
|||
contentType: stringToMIMEType('text/plain'),
|
||||
fileName: 'my-resume.txt',
|
||||
url: 'my-resume.txt',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1581,7 +1634,6 @@ OtherFileTypeWithLongFilename.args = {
|
|||
fileName:
|
||||
'INSERT-APP-NAME_INSERT-APP-APPLE-ID_AppStore_AppsGamesWatch.psd.zip',
|
||||
url: 'a2/a2334324darewer4234',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
|
|
@ -221,10 +221,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
// check if any attachment needs to be downloaded from servers
|
||||
for (const attachment of attachments) {
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: id,
|
||||
});
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
|
||||
attachmentsInProgress += 1;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue