mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
speed up attachment loading by only loading those inview
This commit is contained in:
parent
a986931569
commit
cdd11eee47
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue