speed up attachment loading by only loading those inview

This commit is contained in:
audric 2021-08-27 16:57:29 +10:00
parent a986931569
commit cdd11eee47
6 changed files with 96 additions and 54 deletions

View file

@ -8,7 +8,7 @@ import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
type Props = {
alt: string;
attachment: AttachmentTypeWithPath | AttachmentType;
url: string;
url: string | undefined; // url is undefined if the message is not visible yet
height?: number;
width?: number;
@ -51,12 +51,22 @@ export const Image = (props: Props) => {
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 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
const srcData = !loading ? urlToLoad : '';
@ -89,7 +99,7 @@ export const Image = (props: Props) => {
</div>
) : (
<img
onError={onError}
onError={onErrorUrlFilterering}
className="module-image__image"
alt={alt}
height={height}

View file

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

View file

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

View file

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

View file

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

View file

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