diff --git a/ts/components/conversation/SessionStagedLinkPreview.tsx b/ts/components/conversation/SessionStagedLinkPreview.tsx
index 714b2f4fa..f577c17cd 100644
--- a/ts/components/conversation/SessionStagedLinkPreview.tsx
+++ b/ts/components/conversation/SessionStagedLinkPreview.tsx
@@ -6,7 +6,7 @@ import { arrayBufferFromFile } from '../../types/Attachment';
import { AttachmentUtil, LinkPreviewUtil } from '../../util';
import { fetchLinkPreviewImage } from '../../util/linkPreviewFetch';
import { StagedLinkPreview } from './StagedLinkPreview';
-import { getImageDimensions, THUMBNAIL_SIDE } from '../../types/attachments/VisualAttachment';
+import { getImageDimensions } from '../../types/attachments/VisualAttachment';
export interface StagedLinkPreviewProps extends StagedLinkPreviewData {
onClose: (url: string) => void;
@@ -62,15 +62,12 @@ export const getPreview = async (
// Ensure that this file is either small enough or is resized to meet our
// requirements for attachments
- const withBlob = await AttachmentUtil.autoScale(
- {
- contentType: fullSizeImage.contentType,
- blob: new Blob([fullSizeImage.data], {
- type: fullSizeImage.contentType,
- }),
- },
- { maxSide: THUMBNAIL_SIDE, maxSize: 100 * 1000 } // this is a preview image. No need for it to be crazy big. 100k is big enough
- );
+ const withBlob = await AttachmentUtil.autoScaleForThumbnail({
+ contentType: fullSizeImage.contentType,
+ blob: new Blob([fullSizeImage.data], {
+ type: fullSizeImage.contentType,
+ }),
+ });
const data = await arrayBufferFromFile(withBlob.blob);
objectUrl = URL.createObjectURL(withBlob.blob);
@@ -115,7 +112,7 @@ export const SessionStagedLinkPreview = (props: StagedLinkPreviewProps) => {
title={props.title}
domain={props.domain}
url={props.url}
- image={props.image as any}
+ image={props.image}
/>
);
};
diff --git a/ts/components/conversation/StagedLinkPreview.tsx b/ts/components/conversation/StagedLinkPreview.tsx
index d708c62af..09596b4aa 100644
--- a/ts/components/conversation/StagedLinkPreview.tsx
+++ b/ts/components/conversation/StagedLinkPreview.tsx
@@ -3,15 +3,17 @@ import classNames from 'classnames';
import { Image } from './Image';
-import { AttachmentType, isImageAttachment } from '../../types/Attachment';
import { SessionSpinner } from '../basic/SessionSpinner';
+import { StagedLinkPreviewImage } from './composition/CompositionBox';
+import { isImage } from '../../types/MIME';
+import { fromArrayBufferToBase64 } from '../../session/utils/String';
type Props = {
isLoaded: boolean;
title: null | string;
url: null | string;
domain: null | string;
- image?: AttachmentType;
+ image?: StagedLinkPreviewImage;
onClose: (url: string) => void;
};
@@ -19,13 +21,17 @@ type Props = {
export const StagedLinkPreview = (props: Props) => {
const { isLoaded, onClose, title, image, domain, url } = props;
- const isImage = image && isImageAttachment(image);
+ const isContentTypeImage = image && isImage(image.contentType);
if (isLoaded && !(title && domain)) {
return null;
}
const isLoading = !isLoaded;
+ const dataToRender = image?.data
+ ? `data:image/jpeg;base64, ${fromArrayBufferToBase64(image?.data)}`
+ : '';
+
return (
{
)}
>
{isLoading ?
: null}
- {isLoaded && image && isImage ? (
+ {isLoaded && image && isContentTypeImage ? (
) : null}
diff --git a/ts/components/conversation/composition/CompositionBox.tsx b/ts/components/conversation/composition/CompositionBox.tsx
index 56f1fc255..56bce352e 100644
--- a/ts/components/conversation/composition/CompositionBox.tsx
+++ b/ts/components/conversation/composition/CompositionBox.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import _, { debounce } from 'lodash';
+import _, { debounce, isEmpty } from 'lodash';
import * as MIME from '../../../types/MIME';
@@ -46,7 +46,10 @@ import { Flex } from '../../basic/Flex';
import { CaptionEditor } from '../../CaptionEditor';
import { StagedAttachmentList } from '../StagedAttachmentList';
import { processNewAttachment } from '../../../types/MessageAttachment';
-import { StagedAttachmentImportedType } from '../../../util/attachmentsUtil';
+import {
+ StagedAttachmentImportedType,
+ StagedPreviewImportedType,
+} from '../../../util/attachmentsUtil';
export interface ReplyingToMessageProps {
convoId: string;
@@ -57,12 +60,20 @@ export interface ReplyingToMessageProps {
attachments?: Array
;
}
+export type StagedLinkPreviewImage = {
+ data: ArrayBuffer;
+ size: number;
+ width: number;
+ height: number;
+ contentType: string;
+};
+
export interface StagedLinkPreviewData {
isLoaded: boolean;
title: string | null;
url: string | null;
domain: string | null;
- image?: AttachmentType;
+ image?: StagedLinkPreviewImage;
}
export interface StagedAttachmentType extends AttachmentType {
@@ -592,25 +603,6 @@ class CompositionBoxInner extends React.Component {
getPreview(firstLink, abortController.signal)
.then(ret => {
- let image: AttachmentType | undefined;
- if (ret) {
- if (ret.image?.width) {
- if (ret.image) {
- const blob = new Blob([ret.image.data], {
- type: ret.image.contentType,
- });
- const imageAttachment = {
- ...ret.image,
- url: URL.createObjectURL(blob),
- fileName: 'preview',
- fileSize: null,
- screenshot: null,
- thumbnail: null,
- };
- image = imageAttachment;
- }
- }
- }
// we finished loading the preview, and checking the abortConrtoller, we are still not aborted.
// => update the staged preview
if (this.linkPreviewAbortController && !this.linkPreviewAbortController.signal.aborted) {
@@ -620,7 +612,7 @@ class CompositionBoxInner extends React.Component {
title: ret?.title || null,
url: ret?.url || null,
domain: (ret?.url && window.Signal.LinkPreviews.getDomain(ret.url)) || '',
- image,
+ image: ret?.image,
},
});
} else if (this.linkPreviewAbortController) {
@@ -827,20 +819,19 @@ class CompositionBoxInner extends React.Component {
'attachments'
);
- // we consider that a link previews without a title at least is not a preview
- const linkPreviews =
- (stagedLinkPreview &&
- stagedLinkPreview.isLoaded &&
- stagedLinkPreview.title?.length && [_.pick(stagedLinkPreview, 'url', 'image', 'title')]) ||
- [];
+ // we consider that a link preview without a title at least is not a preview
+ const linkPreview =
+ stagedLinkPreview?.isLoaded && stagedLinkPreview.title?.length
+ ? _.pick(stagedLinkPreview, 'url', 'image', 'title')
+ : undefined;
try {
- const attachments = await this.getFiles();
+ const { attachments, previews } = await this.getFiles(linkPreview);
this.props.sendMessage({
body: messagePlaintext,
attachments: attachments || [],
quote: extractedQuotedMessageProps,
- preview: linkPreviews,
+ preview: previews,
groupInvitation: undefined,
});
@@ -867,20 +858,54 @@ class CompositionBoxInner extends React.Component {
}
// this function is called right before sending a message, to gather really the files behind attachments.
- private async getFiles(): Promise> {
+ private async getFiles(
+ linkPreview?: Pick
+ ): Promise<{
+ attachments: Array;
+ previews: Array;
+ }> {
const { stagedAttachments } = this.props;
+ let attachments: Array = [];
+ let previews: Array = [];
+
if (_.isEmpty(stagedAttachments)) {
- return [];
+ attachments = [];
+ } else {
+ // scale them down
+ const files = await Promise.all(stagedAttachments.map(AttachmentUtil.getFileAndStoreLocally));
+
+ attachments = _.compact(files);
}
- // scale them down
- const files = await Promise.all(stagedAttachments.map(AttachmentUtil.getFileAndStoreLocally));
+
+ if (!linkPreview || _.isEmpty(linkPreview) || !linkPreview.url || !linkPreview.title) {
+ previews = [];
+ } else {
+ const sharedDetails = { url: linkPreview.url, title: linkPreview.title };
+ // store the first image preview locally and get the path and details back to include them in the message
+ const firstLinkPreviewImage = linkPreview.image;
+ if (firstLinkPreviewImage && !isEmpty(firstLinkPreviewImage)) {
+ const storedLinkPreviewAttachment = await AttachmentUtil.getFileAndStoreLocallyImageBuffer(
+ firstLinkPreviewImage.data
+ );
+ if (storedLinkPreviewAttachment) {
+ previews = [{ ...sharedDetails, image: storedLinkPreviewAttachment }];
+ } else {
+ // we couldn't save the image or whatever error happened, just return the url + title
+ previews = [sharedDetails];
+ }
+ } else {
+ // we did not fetch an image from the server
+ previews = [sharedDetails];
+ }
+ }
+
window.inboxStore?.dispatch(
removeAllStagedAttachmentsInConversation({
conversationKey: this.props.selectedConversationKey,
})
);
- return _.compact(files);
+ return { attachments, previews };
}
private async sendVoiceMessage(audioBlob: Blob) {
diff --git a/ts/types/MessageAttachment.ts b/ts/types/MessageAttachment.ts
index 1ea5c10b9..f74538bda 100644
--- a/ts/types/MessageAttachment.ts
+++ b/ts/types/MessageAttachment.ts
@@ -1,5 +1,5 @@
import { remote } from 'electron';
-import { isArrayBuffer, isUndefined, omit } from 'lodash';
+import { isArrayBuffer, isUndefined, omit, isEmpty } from 'lodash';
import {
createAbsolutePathGetter,
createDeleter,
@@ -112,46 +112,52 @@ export const getAttachmentPath = () => {
export const loadAttachmentData = loadData;
-export const loadPreviewData = async (preview: any) => {
- if (!preview || !preview.length) {
+export const loadPreviewData = async (preview: any): Promise> => {
+ if (!preview || !preview.length || isEmpty(preview[0])) {
return [];
}
- return Promise.all(
- preview.map(async (item: any) => {
- if (!item.image) {
- return item;
- }
+ const firstPreview = preview[0];
+ if (!firstPreview.image) {
+ return [firstPreview];
+ }
- return {
- ...item,
- image: await loadAttachmentData(item.image),
- };
- })
- );
+ return [
+ {
+ ...firstPreview,
+ image: await loadAttachmentData(firstPreview.image),
+ },
+ ];
};
export const loadQuoteData = async (quote: any) => {
if (!quote) {
return null;
}
+ console.warn('loadQuoteData', quote);
- return {
- ...quote,
- attachments: await Promise.all(
- (quote.attachments || []).map(async (attachment: any) => {
- const { thumbnail } = attachment;
+ if (!quote.attachments?.length || isEmpty(quote.attachments[0])) {
+ return quote;
+ }
- if (!thumbnail || !thumbnail.path) {
- return attachment;
- }
+ const quotedFirstAttachment = await quote.attachments[0];
- return {
- ...attachment,
- thumbnail: await loadAttachmentData(thumbnail),
- };
- })
- ),
+ const { thumbnail } = quotedFirstAttachment;
+
+ if (!thumbnail || !thumbnail.path) {
+ return {
+ ...quote,
+ attachments: [quotedFirstAttachment],
+ };
+ }
+ const quotedAttachmentWithThumbnail = {
+ ...quotedFirstAttachment,
+ thumbnail: await loadAttachmentData(thumbnail),
+ };
+
+ return {
+ ...quote,
+ attachments: [quotedAttachmentWithThumbnail],
};
};
diff --git a/ts/util/attachmentsUtil.ts b/ts/util/attachmentsUtil.ts
index cfb3d4b26..1b2ee57e1 100644
--- a/ts/util/attachmentsUtil.ts
+++ b/ts/util/attachmentsUtil.ts
@@ -95,6 +95,10 @@ export async function autoScaleForIncomingAvatar(incomingAvatar: ArrayBuffer) {
);
}
+/**
+ * Auto scale an attachment to get a thumbnail from it. We consider that a thumbnail is currently at most 200 ko, is a square and has a maxSize of THUMBNAIL_SIDE
+ * @param attachment the attachment to auto scale
+ */
export async function autoScaleForThumbnail(
attachment: T
) {
@@ -218,6 +222,24 @@ export type StagedAttachmentImportedType = Omit<
'file' | 'url' | 'fileSize'
> & { flags?: number };
+/**
+ * This is the type of the image of a link preview once it was saved in the attachment folder
+ */
+export type StagedImagePreviewImportedType = Pick<
+ StagedAttachmentType,
+ 'contentType' | 'path' | 'size' | 'width' | 'height'
+>;
+
+/**
+ * This is the type of a complete preview imported in the app, hence with the image being a StagedImagePreviewImportedType.
+ * This is the one to be used in uploadData and which should be saved in the database message models
+ */
+export type StagedPreviewImportedType = {
+ url: string;
+ title: string;
+ image?: StagedImagePreviewImportedType;
+};
+
export async function getFileAndStoreLocally(
attachment: StagedAttachmentType
): Promise {
@@ -265,6 +287,35 @@ export async function getFileAndStoreLocally(
};
}
+export async function getFileAndStoreLocallyImageBuffer(imageBuffer: ArrayBuffer) {
+ if (!imageBuffer || !imageBuffer.byteLength) {
+ return null;
+ }
+
+ const contentType = imageType(new Uint8Array(imageBuffer))?.mime || IMAGE_UNKNOWN;
+
+ const blob = new Blob([imageBuffer], { type: contentType });
+
+ const scaled = await autoScaleForThumbnail({
+ contentType,
+ blob,
+ });
+
+ // this operation might change the file size, so be sure to rely on it on return here.
+ const attachmentSavedLocally = await processNewAttachment({
+ data: await scaled.blob.arrayBuffer(),
+ contentType: scaled.contentType,
+ });
+
+ return {
+ contentType: scaled.contentType,
+ path: attachmentSavedLocally.path,
+ width: scaled.width,
+ height: scaled.height,
+ size: attachmentSavedLocally.size,
+ };
+}
+
export type AttachmentFileType = {
attachment: any;
data: ArrayBuffer;