diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx
index cdba596bf..bed54e179 100644
--- a/ts/components/conversation/Image.tsx
+++ b/ts/components/conversation/Image.tsx
@@ -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) => {
) : (
;
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}
/>
diff --git a/ts/components/conversation/ReadableMessage.tsx b/ts/components/conversation/ReadableMessage.tsx
index 923a62336..0b8ae66d4 100644
--- a/ts/components/conversation/ReadableMessage.tsx
+++ b/ts/components/conversation/ReadableMessage.tsx
@@ -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}
+
+ {props.children}
+
);
};
diff --git a/ts/components/conversation/message/MessageAttachment.tsx b/ts/components/conversation/message/MessageAttachment.tsx
index dddda729a..1291d8b4c 100644
--- a/ts/components/conversation/message/MessageAttachment.tsx
+++ b/ts/components/conversation/message/MessageAttachment.tsx
@@ -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) {
diff --git a/ts/hooks/useEncryptedFileFetch.ts b/ts/hooks/useEncryptedFileFetch.ts
index e1fb5988b..0742525a4 100644
--- a/ts/hooks/useEncryptedFileFetch.ts
+++ b/ts/hooks/useEncryptedFileFetch.ts
@@ -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);
diff --git a/ts/session/crypto/DecryptedAttachmentsManager.ts b/ts/session/crypto/DecryptedAttachmentsManager.ts
index c0725f502..833a471c5 100644
--- a/ts/session/crypto/DecryptedAttachmentsManager.ts
+++ b/ts/session/crypto/DecryptedAttachmentsManager.ts
@@ -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();
+const urlToDecryptingPromise = new Map>();
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;
}
+
+ 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;
}
} else {
// Not sure what we got here. Just return the file.