fix: show loading state until image is decrypted and can be mounted

pull/3170/head
yougotwill 8 months ago
parent 45d1791cdf
commit 737dbd45c1

@ -1,17 +1,20 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { useDisableDrag } from '../../hooks/useDisableDrag'; import { useDisableDrag } from '../../hooks/useDisableDrag';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
import { AttachmentType, AttachmentTypeWithPath } from '../../types/Attachment'; import { AttachmentType, AttachmentTypeWithPath } from '../../types/Attachment';
import { Spinner } from '../loading'; import { Spinner } from '../loading';
import { MessageModelType } from '../../models/messageType';
import { MessageGenericAttachment } from './message/message-content/MessageGenericAttachment';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
type Props = { type Props = {
alt: string; alt: string;
attachment: AttachmentTypeWithPath | AttachmentType; attachment: AttachmentTypeWithPath | AttachmentType;
url: string | undefined; // url is undefined if the message is not visible yet url: string | undefined; // url is undefined if the message is not visible yet
imageBroken?: boolean;
height?: number | string; height?: number | string;
width?: number | string; width?: number | string;
@ -26,10 +29,14 @@ type Props = {
forceSquare?: boolean; forceSquare?: boolean;
dropShadow?: boolean; dropShadow?: boolean;
attachmentIndex?: number; attachmentIndex?: number;
direction?: MessageModelType;
highlight?: boolean;
onClick?: (attachment: AttachmentTypeWithPath | AttachmentType) => void; onClick?: (attachment: AttachmentTypeWithPath | AttachmentType) => void;
onClickClose?: (attachment: AttachmentTypeWithPath | AttachmentType) => void; onClickClose?: (attachment: AttachmentTypeWithPath | AttachmentType) => void;
onError?: () => void; onError?: () => void;
timestamp?: number;
}; };
const StyledOverlay = styled.div<Pick<Props, 'darkOverlay' | 'softCorners'>>` const StyledOverlay = styled.div<Pick<Props, 'darkOverlay' | 'softCorners'>>`
@ -46,6 +53,7 @@ export const Image = (props: Props) => {
const { const {
alt, alt,
attachment, attachment,
imageBroken,
closeButton, closeButton,
darkOverlay, darkOverlay,
height: _height, height: _height,
@ -58,35 +66,78 @@ export const Image = (props: Props) => {
forceSquare, forceSquare,
dropShadow, dropShadow,
attachmentIndex, attachmentIndex,
direction,
highlight,
url, url,
width: _width, width: _width,
timestamp,
} = props; } = props;
const disableDrag = useDisableDrag(); const disableDrag = useDisableDrag();
const { loading, urlToLoad } = useEncryptedFileFetch(
url,
attachment.contentType,
false,
timestamp
);
const { caption } = attachment || { caption: null }; const { caption } = attachment || { caption: null };
let { pending } = attachment || { pending: true }; const [pending, setPending] = useState<boolean>(attachment.pending || !url || true);
const [mounted, setMounted] = useState<boolean>(
if (!url) { (!loading || !pending) && urlToLoad === undefined
// force pending to true if the url is undefined, so we show a loader while decrypting the attachemtn );
pending = true;
}
const canClick = onClick && !pending; const canClick = onClick && !pending;
const role = canClick ? 'button' : undefined; const role = canClick ? 'button' : undefined;
const { loading, urlToLoad } = useEncryptedFileFetch(url || '', attachment.contentType, false);
// data will be url if loading is finished and '' if not
const srcData = !loading ? urlToLoad : '';
const onErrorUrlFilterering = useCallback(() => { const onErrorUrlFilterering = useCallback(() => {
if (!loading && !pending && !url && onError) { if (mounted && url && urlToLoad === '' && onError) {
onError(); onError();
setPending(false);
} }
}, [loading, pending, url, onError]); }, [mounted, onError, url, urlToLoad]);
const width = isNumber(_width) ? `${_width}px` : _width; const width = isNumber(_width) ? `${_width}px` : _width;
const height = isNumber(_height) ? `${_height}px` : _height; const height = isNumber(_height) ? `${_height}px` : _height;
useEffect(() => {
if (mounted && url === '') {
setPending(false);
onErrorUrlFilterering();
window.log.debug(
`WIP: [Image] timestamp ${timestamp} fail url ${url !== '' ? url : 'empty'} urlToLoad ${urlToLoad !== '' ? urlToLoad : 'empty'} loading ${loading} pending ${pending} imageBroken ${imageBroken}`
);
}
if (mounted && imageBroken && urlToLoad === '') {
setPending(false);
onErrorUrlFilterering();
window.log.debug(
`WIP: [Image] timestamp ${timestamp} fail url ${url !== '' ? url : 'empty'} urlToLoad ${urlToLoad !== '' ? urlToLoad : 'empty'} loading ${loading} pending ${pending} imageBroken ${imageBroken}`
);
}
if (url) {
setPending(false);
setMounted(!loading && !pending);
window.log.debug(
`WIP: [Image] timestamp ${timestamp} success url ${url !== '' ? url : 'empty'} urlToLoad ${urlToLoad !== '' ? urlToLoad : 'empty'} loading ${loading} pending ${pending} imageBroken ${imageBroken}`
);
}
}, [imageBroken, loading, mounted, onErrorUrlFilterering, pending, timestamp, url, urlToLoad]);
if (mounted && imageBroken) {
return (
<MessageGenericAttachment
attachment={attachment as AttachmentTypeWithPath}
pending={false}
highlight={!!highlight}
selected={!!dropShadow} // dropshadow is selected
direction={direction}
/>
);
}
return ( return (
<div <div
role={role} role={role}
@ -110,7 +161,7 @@ export const Image = (props: Props) => {
}} }}
data-attachmentindex={attachmentIndex} data-attachmentindex={attachmentIndex}
> >
{pending || loading ? ( {!mounted || loading || pending ? (
<div <div
className="module-image__loading-placeholder" className="module-image__loading-placeholder"
style={{ style={{
@ -140,7 +191,7 @@ export const Image = (props: Props) => {
width: forceSquare ? width : '', width: forceSquare ? width : '',
height: forceSquare ? height : '', height: forceSquare ? height : '',
}} }}
src={srcData} src={urlToLoad}
onDragStart={disableDrag} onDragStart={disableDrag}
/> />
)} )}
@ -169,7 +220,7 @@ export const Image = (props: Props) => {
className="module-image__close-button" className="module-image__close-button"
/> />
) : null} ) : null}
{!(pending || loading) && playIconOverlay ? ( {!pending && playIconOverlay ? (
<div className="module-image__play-overlay__circle"> <div className="module-image__play-overlay__circle">
<div className="module-image__play-overlay__icon" /> <div className="module-image__play-overlay__icon" />
</div> </div>

@ -10,13 +10,19 @@ import {
} from '../../types/Attachment'; } from '../../types/Attachment';
import { useIsMessageVisible } from '../../contexts/isMessageVisibleContext'; import { useIsMessageVisible } from '../../contexts/isMessageVisibleContext';
import { useMessageSelected } from '../../state/selectors'; import {
useMessageDirection,
useMessageSelected,
useMessageTimestamp,
} from '../../state/selectors';
import { THUMBNAIL_SIDE } from '../../types/attachments/VisualAttachment'; import { THUMBNAIL_SIDE } from '../../types/attachments/VisualAttachment';
import { Image } from './Image'; import { Image } from './Image';
type Props = { type Props = {
attachments: Array<AttachmentTypeWithPath>; attachments: Array<AttachmentTypeWithPath>;
onError: () => void; onError: () => void;
imageBroken: boolean;
highlight: boolean;
onClickAttachment?: (attachment: AttachmentTypeWithPath | AttachmentType) => void; onClickAttachment?: (attachment: AttachmentTypeWithPath | AttachmentType) => void;
messageId?: string; messageId?: string;
}; };
@ -33,22 +39,27 @@ const Row = (
renderedSize: number; renderedSize: number;
startIndex: number; startIndex: number;
totalAttachmentsCount: number; totalAttachmentsCount: number;
selected: boolean;
} }
) => { ) => {
const { const {
attachments, attachments,
imageBroken,
highlight,
onError, onError,
renderedSize, renderedSize,
startIndex, startIndex,
onClickAttachment, onClickAttachment,
totalAttachmentsCount, totalAttachmentsCount,
selected, messageId,
} = props; } = props;
const isMessageVisible = useIsMessageVisible(); const isMessageVisible = useIsMessageVisible();
const moreMessagesOverlay = totalAttachmentsCount > 3; const moreMessagesOverlay = totalAttachmentsCount > 3;
const moreMessagesOverlayText = moreMessagesOverlay ? `+${totalAttachmentsCount - 3}` : undefined; const moreMessagesOverlayText = moreMessagesOverlay ? `+${totalAttachmentsCount - 3}` : undefined;
const selected = useMessageSelected(messageId);
const direction = useMessageDirection(messageId);
const timestamp = useMessageTimestamp(messageId);
return ( return (
<> <>
{attachments.map((attachment, index) => { {attachments.map((attachment, index) => {
@ -64,11 +75,15 @@ const Row = (
url={isMessageVisible ? getThumbnailUrl(attachment) : undefined} url={isMessageVisible ? getThumbnailUrl(attachment) : undefined}
attachmentIndex={startIndex + index} attachmentIndex={startIndex + index}
onClick={onClickAttachment} onClick={onClickAttachment}
imageBroken={imageBroken}
highlight={highlight}
onError={onError} onError={onError}
softCorners={true} softCorners={true}
darkOverlay={showOverlay} darkOverlay={showOverlay}
overlayText={showOverlay ? moreMessagesOverlayText : undefined} overlayText={showOverlay ? moreMessagesOverlayText : undefined}
dropShadow={selected} dropShadow={selected}
direction={direction}
timestamp={timestamp}
/> />
); );
})} })}
@ -77,9 +92,7 @@ const Row = (
}; };
export const ImageGrid = (props: Props) => { export const ImageGrid = (props: Props) => {
const { attachments, onError, onClickAttachment, messageId } = props; const { attachments, imageBroken, highlight, onError, onClickAttachment, messageId } = props;
const selected = useMessageSelected(messageId);
if (!attachments || !attachments.length) { if (!attachments || !attachments.length) {
return null; return null;
@ -90,12 +103,14 @@ export const ImageGrid = (props: Props) => {
<StyledImageGrid flexDirection={'row'}> <StyledImageGrid flexDirection={'row'}>
<Row <Row
attachments={attachments.slice(0, 1)} attachments={attachments.slice(0, 1)}
imageBroken={imageBroken}
highlight={highlight}
onError={onError} onError={onError}
onClickAttachment={onClickAttachment} onClickAttachment={onClickAttachment}
renderedSize={THUMBNAIL_SIDE} renderedSize={THUMBNAIL_SIDE}
startIndex={0} startIndex={0}
totalAttachmentsCount={attachments.length} totalAttachmentsCount={attachments.length}
selected={selected} messageId={messageId}
/> />
</StyledImageGrid> </StyledImageGrid>
); );
@ -107,12 +122,14 @@ export const ImageGrid = (props: Props) => {
<StyledImageGrid flexDirection={'row'}> <StyledImageGrid flexDirection={'row'}>
<Row <Row
attachments={attachments.slice(0, 2)} attachments={attachments.slice(0, 2)}
imageBroken={imageBroken}
highlight={highlight}
onError={onError} onError={onError}
onClickAttachment={onClickAttachment} onClickAttachment={onClickAttachment}
renderedSize={THUMBNAIL_SIDE} renderedSize={THUMBNAIL_SIDE}
startIndex={0} startIndex={0}
totalAttachmentsCount={attachments.length} totalAttachmentsCount={attachments.length}
selected={selected} messageId={messageId}
/> />
</StyledImageGrid> </StyledImageGrid>
); );
@ -125,23 +142,27 @@ export const ImageGrid = (props: Props) => {
<StyledImageGrid flexDirection={'row'}> <StyledImageGrid flexDirection={'row'}>
<Row <Row
attachments={attachments.slice(0, 1)} attachments={attachments.slice(0, 1)}
imageBroken={imageBroken}
highlight={highlight}
onError={onError} onError={onError}
onClickAttachment={onClickAttachment} onClickAttachment={onClickAttachment}
renderedSize={THUMBNAIL_SIDE} renderedSize={THUMBNAIL_SIDE}
startIndex={0} startIndex={0}
totalAttachmentsCount={attachments.length} totalAttachmentsCount={attachments.length}
selected={selected} messageId={messageId}
/> />
<StyledImageGrid flexDirection={'column'}> <StyledImageGrid flexDirection={'column'}>
<Row <Row
attachments={attachments.slice(1, 3)} attachments={attachments.slice(1, 3)}
imageBroken={imageBroken}
highlight={highlight}
onError={onError} onError={onError}
onClickAttachment={onClickAttachment} onClickAttachment={onClickAttachment}
renderedSize={columnImageSide} renderedSize={columnImageSide}
startIndex={1} startIndex={1}
totalAttachmentsCount={attachments.length} totalAttachmentsCount={attachments.length}
selected={selected} messageId={messageId}
/> />
</StyledImageGrid> </StyledImageGrid>
</StyledImageGrid> </StyledImageGrid>

@ -15,8 +15,6 @@ import {
import { import {
AttachmentType, AttachmentType,
AttachmentTypeWithPath, AttachmentTypeWithPath,
hasImage,
hasVideoScreenshot,
isAudio, isAudio,
isImage, isImage,
isVideo, isVideo,
@ -128,14 +126,12 @@ export const MessageAttachment = (props: Props) => {
return <ClickToTrustSender messageId={messageId} />; return <ClickToTrustSender messageId={messageId} />;
} }
if ( if (isImage(attachments) || isVideo(attachments)) {
(isImage(attachments) && hasImage(attachments)) ||
(isVideo(attachments) && hasVideoScreenshot(attachments))
) {
// we use the carousel in the detail view // we use the carousel in the detail view
if (isDetailView) { if (isDetailView) {
return null; return null;
} }
return ( return (
<MessageHighlighter highlight={highlight}> <MessageHighlighter highlight={highlight}>
<StyledImageGridContainer messageDirection={direction}> <StyledImageGridContainer messageDirection={direction}>
@ -177,6 +173,7 @@ export const MessageAttachment = (props: Props) => {
return ( return (
<MessageGenericAttachment <MessageGenericAttachment
attachment={firstAttachment} attachment={firstAttachment}
pending={firstAttachment.pending}
direction={direction} direction={direction}
highlight={highlight} highlight={highlight}
selected={selected} selected={selected}

@ -21,7 +21,6 @@ import {
getShouldHighlightMessage, getShouldHighlightMessage,
} from '../../../../state/selectors/conversations'; } from '../../../../state/selectors/conversations';
import { useSelectedIsPrivate } from '../../../../state/selectors/selectedConversation'; import { useSelectedIsPrivate } from '../../../../state/selectors/selectedConversation';
import { canDisplayImagePreview } from '../../../../types/Attachment';
import { MessageAttachment } from './MessageAttachment'; import { MessageAttachment } from './MessageAttachment';
import { MessageAvatar } from './MessageAvatar'; import { MessageAvatar } from './MessageAvatar';
import { MessageHighlighter } from './MessageHighlighter'; import { MessageHighlighter } from './MessageHighlighter';
@ -147,17 +146,12 @@ export const MessageContent = (props: Props) => {
return null; return null;
} }
const { direction, text, timestamp, serverTimestamp, previews, quote, attachments } = const { direction, text, timestamp, serverTimestamp, previews, quote } = contentProps;
contentProps;
const hasContentBeforeAttachment = !isEmpty(previews) || !isEmpty(quote) || !isEmpty(text); const hasContentBeforeAttachment = !isEmpty(previews) || !isEmpty(quote) || !isEmpty(text);
const toolTipTitle = moment(serverTimestamp || timestamp).format('llll'); const toolTipTitle = moment(serverTimestamp || timestamp).format('llll');
window.log.debug(
`WIP: [MessageAttachment] ${props.messageId} timestamp ${timestamp} imageBroken ${imageBroken} display ${canDisplayImagePreview(attachments)} attachments.contentType ${attachments?.[0].contentType || 'none'}`
);
return ( return (
<StyledMessageContent <StyledMessageContent
className={classNames('module-message__container', `module-message__container--${direction}`)} className={classNames('module-message__container', `module-message__container--${direction}`)}

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { import {
getAlreadyDecryptedMediaUrl, getAlreadyDecryptedMediaUrl,
@ -6,40 +6,55 @@ import {
} from '../session/crypto/DecryptedAttachmentsManager'; } from '../session/crypto/DecryptedAttachmentsManager';
import { perfEnd, perfStart } from '../session/utils/Performance'; import { perfEnd, perfStart } from '../session/utils/Performance';
export const useEncryptedFileFetch = (url: string, contentType: string, isAvatar: boolean) => { export const useEncryptedFileFetch = (
const [urlToLoad, setUrlToLoad] = useState(''); url: string | undefined,
const [loading, setLoading] = useState(false); contentType: string,
isAvatar: boolean,
const mountedRef = useRef(true); timestamp?: number
) => {
const [urlToLoad, setUrlToLoad] = useState<string | undefined>(undefined);
const [loading, setLoading] = useState(true);
const alreadyDecrypted = url ? getAlreadyDecryptedMediaUrl(url) : '';
const fetchUrl = useCallback(
async (mediaUrl: string | undefined) => {
if (alreadyDecrypted || !mediaUrl) {
window.log.debug(
`WIP: [Image] timestamp ${timestamp} alreadyDecrypted ${alreadyDecrypted !== '' ? alreadyDecrypted : 'empty'} mediaUrl ${mediaUrl !== '' ? mediaUrl : 'empty'}`
);
if (alreadyDecrypted) {
setUrlToLoad(alreadyDecrypted);
setLoading(false);
}
return;
}
const alreadyDecrypted = getAlreadyDecryptedMediaUrl(url); setLoading(true);
useEffect(() => { try {
async function fetchUrl() { perfStart(`getDecryptedMediaUrl-${mediaUrl}`);
perfStart(`getDecryptedMediaUrl-${url}`); const decryptedUrl = await getDecryptedMediaUrl(mediaUrl, contentType, isAvatar);
const decryptedUrl = await getDecryptedMediaUrl(url, contentType, isAvatar); perfEnd(`getDecryptedMediaUrl-${mediaUrl}`, `getDecryptedMediaUrl-${mediaUrl}`);
perfEnd(`getDecryptedMediaUrl-${url}`, `getDecryptedMediaUrl-${url}`); window.log.debug(
`WIP: [Image] timestamp ${timestamp} decryptedUrl ${decryptedUrl !== '' ? decryptedUrl : 'empty'}`
);
if (mountedRef.current) {
setUrlToLoad(decryptedUrl); setUrlToLoad(decryptedUrl);
} catch (error) {
window.log.error(`WIP: [Image] timestamp ${timestamp} error ${error}`);
setUrlToLoad('');
} finally {
setLoading(false); setLoading(false);
} }
} },
if (alreadyDecrypted) { [alreadyDecrypted, contentType, isAvatar, timestamp]
return; );
}
setLoading(true); useEffect(() => {
mountedRef.current = true; void fetchUrl(url);
void fetchUrl(); }, [fetchUrl, url]);
// eslint-disable-next-line consistent-return
return () => {
mountedRef.current = false;
};
}, [url, alreadyDecrypted, contentType, isAvatar]);
if (alreadyDecrypted) {
return { urlToLoad: alreadyDecrypted, loading: false };
}
return { urlToLoad, loading }; return { urlToLoad, loading };
}; };

@ -10,7 +10,6 @@
* *
*/ */
import path from 'path'; import path from 'path';
import { reject } from 'lodash';
import * as fse from 'fs-extra'; import * as fse from 'fs-extra';
@ -114,7 +113,7 @@ export const getDecryptedMediaUrl = async (
urlToDecryptingPromise.set( urlToDecryptingPromise.set(
url, url,
new Promise(async resolve => { new Promise(async (resolve, reject) => {
// window.log.debug('about to read and decrypt file :', url, path.isAbsolute(url)); // window.log.debug('about to read and decrypt file :', url, path.isAbsolute(url));
try { try {
const absUrl = path.isAbsolute(url) ? url : getAbsoluteAttachmentPath(url); const absUrl = path.isAbsolute(url) ? url : getAbsoluteAttachmentPath(url);
@ -149,7 +148,6 @@ export const getDecryptedMediaUrl = async (
} }
}) })
); );
return urlToDecryptingPromise.get(url) as Promise<string>; return urlToDecryptingPromise.get(url) as Promise<string>;
} }
// Not sure what we got here. Just return the file. // Not sure what we got here. Just return the file.

Loading…
Cancel
Save