speed up attachment loading by only loading those inview

pull/1884/head
audric 4 years ago
parent a986931569
commit cdd11eee47

@ -8,7 +8,7 @@ import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
type Props = { type Props = {
alt: string; alt: string;
attachment: AttachmentTypeWithPath | AttachmentType; attachment: AttachmentTypeWithPath | AttachmentType;
url: string; url: string | undefined; // url is undefined if the message is not visible yet
height?: number; height?: number;
width?: number; width?: number;
@ -51,12 +51,22 @@ export const Image = (props: Props) => {
return false; return false;
}, []); }, []);
const { caption, pending } = attachment || { caption: null, pending: true }; const onErrorUrlFilterering = useCallback(() => {
if (url && onError) {
onError();
}
return;
}, [url, onError]);
const { caption } = attachment || { caption: null };
let { pending } = attachment || { pending: true };
if (!url) {
// 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);
const { loading, urlToLoad } = useEncryptedFileFetch(url, attachment.contentType);
// data will be url if loading is finished and '' if not // data will be url if loading is finished and '' if not
const srcData = !loading ? urlToLoad : ''; const srcData = !loading ? urlToLoad : '';
@ -89,7 +99,7 @@ export const Image = (props: Props) => {
</div> </div>
) : ( ) : (
<img <img
onError={onError} onError={onErrorUrlFilterering}
className="module-image__image" className="module-image__image"
alt={alt} alt={alt}
height={height} height={height}

@ -1,4 +1,4 @@
import React from 'react'; import React, { useContext } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
@ -12,19 +12,20 @@ import {
} from '../../types/Attachment'; } from '../../types/Attachment';
import { Image } from './Image'; import { Image } from './Image';
import { IsMessageVisibleContext } from './ReadableMessage';
type Props = { type Props = {
attachments: Array<AttachmentTypeWithPath>; attachments: Array<AttachmentTypeWithPath>;
bottomOverlay?: boolean; bottomOverlay?: boolean;
onError: () => void; onError: () => void;
onClickAttachment?: (attachment: AttachmentTypeWithPath | AttachmentType) => void; onClickAttachment?: (attachment: AttachmentTypeWithPath | AttachmentType) => void;
}; };
// tslint:disable: cyclomatic-complexity max-func-body-length use-simple-attributes
export const ImageGrid = (props: Props) => { export const ImageGrid = (props: Props) => {
// tslint:disable-next-line max-func-body-length */
const { attachments, bottomOverlay, onError, onClickAttachment } = props; const { attachments, bottomOverlay, onError, onClickAttachment } = props;
const isMessageVisible = useContext(IsMessageVisibleContext);
const withBottomOverlay = Boolean(bottomOverlay); const withBottomOverlay = Boolean(bottomOverlay);
if (!attachments || !attachments.length) { if (!attachments || !attachments.length) {
@ -43,7 +44,7 @@ export const ImageGrid = (props: Props) => {
playIconOverlay={isVideoAttachment(attachments[0])} playIconOverlay={isVideoAttachment(attachments[0])}
height={height} height={height}
width={width} width={width}
url={getThumbnailUrl(attachments[0])} url={isMessageVisible ? getThumbnailUrl(attachments[0]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -61,7 +62,7 @@ export const ImageGrid = (props: Props) => {
playIconOverlay={isVideoAttachment(attachments[0])} playIconOverlay={isVideoAttachment(attachments[0])}
height={149} height={149}
width={149} width={149}
url={getThumbnailUrl(attachments[0])} url={isMessageVisible ? getThumbnailUrl(attachments[0]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -72,7 +73,7 @@ export const ImageGrid = (props: Props) => {
height={149} height={149}
width={149} width={149}
attachment={attachments[1]} attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])} url={isMessageVisible ? getThumbnailUrl(attachments[1]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -90,7 +91,7 @@ export const ImageGrid = (props: Props) => {
playIconOverlay={isVideoAttachment(attachments[0])} playIconOverlay={isVideoAttachment(attachments[0])}
height={200} height={200}
width={199} width={199}
url={getThumbnailUrl(attachments[0])} url={isMessageVisible ? getThumbnailUrl(attachments[0]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -101,7 +102,7 @@ export const ImageGrid = (props: Props) => {
width={99} width={99}
attachment={attachments[1]} attachment={attachments[1]}
playIconOverlay={isVideoAttachment(attachments[1])} playIconOverlay={isVideoAttachment(attachments[1])}
url={getThumbnailUrl(attachments[1])} url={isMessageVisible ? getThumbnailUrl(attachments[1]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -112,7 +113,7 @@ export const ImageGrid = (props: Props) => {
width={99} width={99}
attachment={attachments[2]} attachment={attachments[2]}
playIconOverlay={isVideoAttachment(attachments[2])} playIconOverlay={isVideoAttachment(attachments[2])}
url={getThumbnailUrl(attachments[2])} url={isMessageVisible ? getThumbnailUrl(attachments[2]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -132,7 +133,7 @@ export const ImageGrid = (props: Props) => {
playIconOverlay={isVideoAttachment(attachments[0])} playIconOverlay={isVideoAttachment(attachments[0])}
height={149} height={149}
width={149} width={149}
url={getThumbnailUrl(attachments[0])} url={isMessageVisible ? getThumbnailUrl(attachments[0]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -142,7 +143,7 @@ export const ImageGrid = (props: Props) => {
height={149} height={149}
width={149} width={149}
attachment={attachments[1]} attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])} url={isMessageVisible ? getThumbnailUrl(attachments[1]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -155,7 +156,7 @@ export const ImageGrid = (props: Props) => {
height={149} height={149}
width={149} width={149}
attachment={attachments[2]} attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])} url={isMessageVisible ? getThumbnailUrl(attachments[2]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -166,7 +167,7 @@ export const ImageGrid = (props: Props) => {
height={149} height={149}
width={149} width={149}
attachment={attachments[3]} attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])} url={isMessageVisible ? getThumbnailUrl(attachments[3]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -189,7 +190,7 @@ export const ImageGrid = (props: Props) => {
playIconOverlay={isVideoAttachment(attachments[0])} playIconOverlay={isVideoAttachment(attachments[0])}
height={149} height={149}
width={149} width={149}
url={getThumbnailUrl(attachments[0])} url={isMessageVisible ? getThumbnailUrl(attachments[0]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -199,7 +200,7 @@ export const ImageGrid = (props: Props) => {
height={149} height={149}
width={149} width={149}
attachment={attachments[1]} attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])} url={isMessageVisible ? getThumbnailUrl(attachments[1]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -212,7 +213,7 @@ export const ImageGrid = (props: Props) => {
height={99} height={99}
width={99} width={99}
attachment={attachments[2]} attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])} url={isMessageVisible ? getThumbnailUrl(attachments[2]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -223,7 +224,7 @@ export const ImageGrid = (props: Props) => {
height={99} height={99}
width={98} width={98}
attachment={attachments[3]} attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])} url={isMessageVisible ? getThumbnailUrl(attachments[3]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />
@ -236,7 +237,7 @@ export const ImageGrid = (props: Props) => {
darkOverlay={moreMessagesOverlay} darkOverlay={moreMessagesOverlay}
overlayText={moreMessagesOverlayText} overlayText={moreMessagesOverlayText}
attachment={attachments[4]} attachment={attachments[4]}
url={getThumbnailUrl(attachments[4])} url={isMessageVisible ? getThumbnailUrl(attachments[4]) : undefined}
onClick={onClickAttachment} onClick={onClickAttachment}
onError={onError} onError={onError}
/> />

@ -1,5 +1,5 @@
import _, { noop } from 'lodash'; import _, { noop } from 'lodash';
import React, { useCallback, useState } from 'react'; import React, { createContext, useCallback, useState } from 'react';
import { InView } from 'react-intersection-observer'; import { InView } from 'react-intersection-observer';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getMessageById } from '../../data/data'; import { getMessageById } from '../../data/data';
@ -42,6 +42,8 @@ const debouncedTriggerLoadMore = _.debounce(
100 100
); );
export const IsMessageVisibleContext = createContext(false);
export const ReadableMessage = (props: ReadableMessageProps) => { export const ReadableMessage = (props: ReadableMessageProps) => {
const { messageId, onContextMenu, className, receivedAt, isUnread } = props; const { messageId, onContextMenu, className, receivedAt, isUnread } = props;
@ -57,7 +59,10 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
const fetchingMore = useSelector(areMoreMessagesBeingFetched); const fetchingMore = useSelector(areMoreMessagesBeingFetched);
const shouldMarkReadWhenVisible = isUnread; const shouldMarkReadWhenVisible = isUnread;
const [isMessageVisible, setMessageIsVisible] = useState(false);
const onVisible = useCallback( const onVisible = useCallback(
// tslint:disable-next-line: cyclomatic-complexity
async (inView: boolean | Object) => { async (inView: boolean | Object) => {
// when the view first loads, it needs to scroll to the unread messages. // when the view first loads, it needs to scroll to the unread messages.
// we need to disable the inview on the first loading // we need to disable the inview on the first loading
@ -91,9 +96,12 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
if ( if (
(inView === true || (inView === true ||
((inView as any).type === 'focus' && (inView as any).returnValue === true)) && ((inView as any).type === 'focus' && (inView as any).returnValue === true)) &&
shouldMarkReadWhenVisible &&
isAppFocused isAppFocused
) { ) {
if (isMessageVisible !== true) {
setMessageIsVisible(true);
}
if (shouldMarkReadWhenVisible) {
const found = await getMessageById(messageId); const found = await getMessageById(messageId);
if (found && Boolean(found.get('unread'))) { if (found && Boolean(found.get('unread'))) {
@ -102,6 +110,7 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
await found.markRead(Date.now()); await found.markRead(Date.now());
} }
} }
}
}, },
[ [
selectedConversationKey, selectedConversationKey,
@ -131,7 +140,9 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
triggerOnce={false} triggerOnce={false}
trackVisibility={true} trackVisibility={true}
> >
<IsMessageVisibleContext.Provider value={isMessageVisible}>
{props.children} {props.children}
</IsMessageVisibleContext.Provider>
</InView> </InView>
); );
}; };

@ -49,6 +49,7 @@ type Props = {
imageBroken: boolean; imageBroken: boolean;
handleImageError: () => void; handleImageError: () => void;
}; };
// tslint:disable: use-simple-attributes
// tslint:disable-next-line max-func-body-length cyclomatic-complexity // tslint:disable-next-line max-func-body-length cyclomatic-complexity
export const MessageAttachment = (props: Props) => { export const MessageAttachment = (props: Props) => {
@ -57,7 +58,6 @@ export const MessageAttachment = (props: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const attachmentProps = useSelector(state => getMessageAttachmentProps(state as any, messageId)); const attachmentProps = useSelector(state => getMessageAttachmentProps(state as any, messageId));
const multiSelectMode = useSelector(isMessageSelectionMode); const multiSelectMode = useSelector(isMessageSelectionMode);
const onClickOnImageGrid = useCallback( const onClickOnImageGrid = useCallback(
(attachment: AttachmentTypeWithPath | AttachmentType) => { (attachment: AttachmentTypeWithPath | AttachmentType) => {
if (multiSelectMode) { if (multiSelectMode) {

@ -11,10 +11,10 @@ export const useEncryptedFileFetch = (url: string, contentType: string) => {
const mountedRef = useRef(true); const mountedRef = useRef(true);
async function fetchUrl() { async function fetchUrl() {
perfStart(`getDecryptedMediaUrl${url}`); perfStart(`getDecryptedMediaUrl-${url}`);
const decryptedUrl = await getDecryptedMediaUrl(url, contentType); const decryptedUrl = await getDecryptedMediaUrl(url, contentType);
perfEnd(`getDecryptedMediaUrl${url}`, 'getDecryptedMediaUrl'); perfEnd(`getDecryptedMediaUrl-${url}`, `getDecryptedMediaUrl-${url}`);
if (mountedRef.current) { if (mountedRef.current) {
setUrlToLoad(decryptedUrl); setUrlToLoad(decryptedUrl);

@ -15,6 +15,7 @@ import { DURATION } from '../constants';
// add a way to remove the blob when the attachment file path is removed (message removed?) // add a way to remove the blob when the attachment file path is removed (message removed?)
// do not hardcode the password // do not hardcode the password
const urlToDecryptedBlobMap = new Map<string, { decrypted: string; lastAccessTimestamp: number }>(); const urlToDecryptedBlobMap = new Map<string, { decrypted: string; lastAccessTimestamp: number }>();
const urlToDecryptingPromise = new Map<string, Promise<string>>();
export const cleanUpOldDecryptedMedias = () => { export const cleanUpOldDecryptedMedias = () => {
const currentTimestamp = Date.now(); const currentTimestamp = Date.now();
@ -31,6 +32,8 @@ export const cleanUpOldDecryptedMedias = () => {
countKept++; countKept++;
} }
} }
urlToDecryptedBlobMap.clear();
urlToDecryptingPromise.clear();
window?.log?.info(`Clean medias blobs: cleaned/kept: ${countCleaned}:${countKept}`); window?.log?.info(`Clean medias blobs: cleaned/kept: ${countCleaned}:${countKept}`);
}; };
@ -59,8 +62,17 @@ export const getDecryptedMediaUrl = async (url: string, contentType: string): Pr
return existingObjUrl; return existingObjUrl;
} else { } else {
if (urlToDecryptingPromise.has(url)) {
return urlToDecryptingPromise.get(url) as Promise<string>;
}
urlToDecryptingPromise.set(
url,
new Promise(async resolve => {
const encryptedFileContent = await fse.readFile(url); const encryptedFileContent = await fse.readFile(url);
const decryptedContent = await decryptAttachmentBuffer(toArrayBuffer(encryptedFileContent)); const decryptedContent = await decryptAttachmentBuffer(
toArrayBuffer(encryptedFileContent)
);
if (decryptedContent?.length) { if (decryptedContent?.length) {
const arrayBuffer = decryptedContent.buffer; const arrayBuffer = decryptedContent.buffer;
const { makeObjectUrl } = window.Signal.Types.VisualAttachment; const { makeObjectUrl } = window.Signal.Types.VisualAttachment;
@ -72,12 +84,20 @@ export const getDecryptedMediaUrl = async (url: string, contentType: string): Pr
lastAccessTimestamp: Date.now(), lastAccessTimestamp: Date.now(),
}); });
} }
return obj; urlToDecryptingPromise.delete(url);
resolve(obj);
return;
} else { } else {
// failed to decrypt, fallback to url image loading // failed to decrypt, fallback to url image loading
// it might be a media we received before the update encrypting attachments locally. // it might be a media we received before the update encrypting attachments locally.
return url; urlToDecryptingPromise.delete(url);
resolve(url);
return;
} }
})
);
return urlToDecryptingPromise.get(url) as Promise<string>;
} }
} else { } else {
// Not sure what we got here. Just return the file. // Not sure what we got here. Just return the file.

Loading…
Cancel
Save