signal-desktop/ts/components/StoryImage.tsx

194 lines
5 KiB
TypeScript
Raw Normal View History

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