2022-03-29 01:10:08 +00:00
|
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2022-05-04 17:43:22 +00:00
|
|
|
import type { ReactNode } from 'react';
|
2022-08-04 00:38:41 +00:00
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
2022-03-29 01:10:08 +00:00
|
|
|
import classNames from 'classnames';
|
|
|
|
import { Blurhash } from 'react-blurhash';
|
|
|
|
|
|
|
|
import type { AttachmentType } from '../types/Attachment';
|
|
|
|
import type { LocalizerType } from '../types/Util';
|
|
|
|
import { Spinner } from './Spinner';
|
2022-04-06 01:18:07 +00:00
|
|
|
import { TextAttachment } from './TextAttachment';
|
2022-03-29 01:10:08 +00:00
|
|
|
import { ThemeType } from '../types/Util';
|
|
|
|
import {
|
|
|
|
defaultBlurHash,
|
2022-08-04 00:38:41 +00:00
|
|
|
hasFailed,
|
2022-03-29 01:10:08 +00:00
|
|
|
hasNotResolved,
|
2022-04-12 19:29:30 +00:00
|
|
|
isDownloaded,
|
2022-03-29 01:10:08 +00:00
|
|
|
isDownloading,
|
2022-04-12 19:29:30 +00:00
|
|
|
isGIF,
|
2022-03-29 01:10:08 +00:00
|
|
|
} from '../types/Attachment';
|
|
|
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
2022-04-12 19:29:30 +00:00
|
|
|
import { isVideoTypeSupported } from '../util/GoogleChrome';
|
2022-03-29 01:10:08 +00:00
|
|
|
|
|
|
|
export type PropsType = {
|
|
|
|
readonly attachment?: AttachmentType;
|
2022-05-04 17:43:22 +00:00
|
|
|
readonly children?: ReactNode;
|
2022-08-04 00:38:41 +00:00
|
|
|
readonly firstName: string;
|
2022-04-12 19:29:30 +00:00
|
|
|
readonly i18n: LocalizerType;
|
2022-08-04 00:38:41 +00:00
|
|
|
readonly isMe?: boolean;
|
2022-05-06 19:02:44 +00:00
|
|
|
readonly isMuted?: boolean;
|
2022-05-02 16:24:41 +00:00
|
|
|
readonly isPaused?: boolean;
|
2022-03-29 01:10:08 +00:00
|
|
|
readonly isThumbnail?: boolean;
|
|
|
|
readonly label: string;
|
|
|
|
readonly moduleClassName?: string;
|
|
|
|
readonly queueStoryDownload: (storyId: string) => unknown;
|
|
|
|
readonly storyId: string;
|
2023-02-24 23:18:57 +00:00
|
|
|
readonly onMediaPlaybackStart: () => void;
|
2022-03-29 01:10:08 +00:00
|
|
|
};
|
|
|
|
|
2022-11-18 00:45:19 +00:00
|
|
|
export function StoryImage({
|
2022-03-29 01:10:08 +00:00
|
|
|
attachment,
|
2022-05-04 17:43:22 +00:00
|
|
|
children,
|
2022-08-04 00:38:41 +00:00
|
|
|
firstName,
|
2022-03-29 01:10:08 +00:00
|
|
|
i18n,
|
2022-08-04 00:38:41 +00:00
|
|
|
isMe,
|
2022-05-06 19:02:44 +00:00
|
|
|
isMuted,
|
2022-05-02 16:24:41 +00:00
|
|
|
isPaused,
|
2022-03-29 01:10:08 +00:00
|
|
|
isThumbnail,
|
|
|
|
label,
|
|
|
|
moduleClassName,
|
|
|
|
queueStoryDownload,
|
|
|
|
storyId,
|
2023-02-24 23:18:57 +00:00
|
|
|
onMediaPlaybackStart,
|
2022-11-18 00:45:19 +00:00
|
|
|
}: PropsType): JSX.Element | null {
|
2022-03-29 01:10:08 +00:00
|
|
|
const shouldDownloadAttachment =
|
2022-07-01 05:36:40 +00:00
|
|
|
(!isDownloaded(attachment) && !isDownloading(attachment)) ||
|
|
|
|
hasNotResolved(attachment);
|
2022-03-29 01:10:08 +00:00
|
|
|
|
2022-08-04 00:38:41 +00:00
|
|
|
const [hasImgError, setHasImgError] = useState(false);
|
2022-05-02 16:24:41 +00:00
|
|
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (shouldDownloadAttachment) {
|
|
|
|
queueStoryDownload(storyId);
|
|
|
|
}
|
|
|
|
}, [queueStoryDownload, shouldDownloadAttachment, storyId]);
|
|
|
|
|
2022-05-02 16:24:41 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (!videoRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isPaused) {
|
|
|
|
videoRef.current.pause();
|
|
|
|
} else {
|
2023-02-24 23:18:57 +00:00
|
|
|
onMediaPlaybackStart();
|
2022-12-21 18:41:48 +00:00
|
|
|
void videoRef.current.play();
|
2022-05-02 16:24:41 +00:00
|
|
|
}
|
2023-02-24 23:18:57 +00:00
|
|
|
}, [isPaused, onMediaPlaybackStart]);
|
2022-05-02 16:24:41 +00:00
|
|
|
|
2022-11-23 07:29:01 +00:00
|
|
|
useEffect(() => {
|
|
|
|
setHasImgError(false);
|
|
|
|
}, [attachment?.url, attachment?.thumbnail?.url]);
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
if (!attachment) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-08-04 00:38:41 +00:00
|
|
|
const hasError = hasImgError || hasFailed(attachment);
|
|
|
|
const isPending =
|
|
|
|
Boolean(attachment.pending) && !attachment.textAttachment && !hasError;
|
|
|
|
const isNotReadyToShow = hasNotResolved(attachment) || isPending || hasError;
|
2022-04-12 19:29:30 +00:00
|
|
|
const isSupportedVideo = isVideoTypeSupported(attachment.contentType);
|
2022-03-29 01:10:08 +00:00
|
|
|
|
|
|
|
const getClassName = getClassNamesFor('StoryImage', moduleClassName);
|
|
|
|
|
|
|
|
let storyElement: JSX.Element;
|
2022-04-06 01:18:07 +00:00
|
|
|
if (attachment.textAttachment) {
|
|
|
|
storyElement = (
|
2022-04-25 17:25:50 +00:00
|
|
|
<TextAttachment
|
|
|
|
i18n={i18n}
|
|
|
|
isThumbnail={isThumbnail}
|
|
|
|
textAttachment={attachment.textAttachment}
|
|
|
|
/>
|
2022-04-06 01:18:07 +00:00
|
|
|
);
|
|
|
|
} else if (isNotReadyToShow) {
|
2022-03-29 01:10:08 +00:00
|
|
|
storyElement = (
|
|
|
|
<Blurhash
|
|
|
|
hash={attachment.blurHash || defaultBlurHash(ThemeType.dark)}
|
|
|
|
height={attachment.height}
|
|
|
|
width={attachment.width}
|
|
|
|
/>
|
|
|
|
);
|
2022-04-12 19:29:30 +00:00
|
|
|
} else if (!isThumbnail && isSupportedVideo) {
|
|
|
|
const shouldLoop = isGIF(attachment ? [attachment] : undefined);
|
|
|
|
|
2022-03-29 01:10:08 +00:00
|
|
|
storyElement = (
|
2022-04-12 19:29:30 +00:00
|
|
|
<video
|
|
|
|
autoPlay
|
|
|
|
className={getClassName('__image')}
|
|
|
|
controls={false}
|
2022-05-02 16:24:41 +00:00
|
|
|
key={attachment.url}
|
2022-04-12 19:29:30 +00:00
|
|
|
loop={shouldLoop}
|
2022-05-06 19:02:44 +00:00
|
|
|
muted={isMuted}
|
2022-05-02 16:24:41 +00:00
|
|
|
ref={videoRef}
|
2022-04-12 19:29:30 +00:00
|
|
|
>
|
|
|
|
<source src={attachment.url} />
|
|
|
|
</video>
|
2022-03-29 01:10:08 +00:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
storyElement = (
|
|
|
|
<img
|
|
|
|
alt={label}
|
|
|
|
className={getClassName('__image')}
|
2022-08-04 00:38:41 +00:00
|
|
|
onError={() => setHasImgError(true)}
|
2022-03-29 01:10:08 +00:00
|
|
|
src={
|
|
|
|
isThumbnail && attachment.thumbnail
|
|
|
|
? attachment.thumbnail.url
|
|
|
|
: attachment.url
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-04-25 17:25:50 +00:00
|
|
|
let overlay: JSX.Element | undefined;
|
2022-03-29 01:10:08 +00:00
|
|
|
if (isPending) {
|
2022-04-25 17:25:50 +00:00
|
|
|
overlay = (
|
|
|
|
<div className="StoryImage__overlay-container">
|
2023-03-30 00:03:25 +00:00
|
|
|
<div className="StoryImage__spinner-bubble" title={i18n('icu:loading')}>
|
2022-03-29 01:10:08 +00:00
|
|
|
<Spinner moduleClassName="StoryImage__spinner" svgSize="small" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
2022-08-04 00:38:41 +00:00
|
|
|
} else if (hasError) {
|
|
|
|
let content = <div className="StoryImage__error" />;
|
|
|
|
if (!isThumbnail) {
|
|
|
|
if (isMe) {
|
2023-03-30 00:03:25 +00:00
|
|
|
content = <>{i18n('icu:StoryImage__error--you')}</>;
|
2022-08-04 00:38:41 +00:00
|
|
|
} else {
|
2023-03-27 23:37:39 +00:00
|
|
|
content = (
|
|
|
|
<>
|
2023-03-30 00:03:25 +00:00
|
|
|
{i18n('icu:StoryImage__error2', {
|
2023-03-27 23:37:39 +00:00
|
|
|
name: firstName,
|
|
|
|
})}
|
|
|
|
</>
|
|
|
|
);
|
2022-08-04 00:38:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
overlay = <div className="StoryImage__overlay-container">{content}</div>;
|
2022-03-29 01:10:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
getClassName(''),
|
|
|
|
isThumbnail ? getClassName('--thumbnail') : undefined
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{storyElement}
|
2022-04-25 17:25:50 +00:00
|
|
|
{overlay}
|
2022-05-04 17:43:22 +00:00
|
|
|
{children}
|
2022-03-29 01:10:08 +00:00
|
|
|
</div>
|
|
|
|
);
|
2022-11-18 00:45:19 +00:00
|
|
|
}
|