diff --git a/js/background.js b/js/background.js index 76d8e3e41..7324faa85 100644 --- a/js/background.js +++ b/js/background.js @@ -653,64 +653,69 @@ const data = await readFile({ file: avatar }); // Ensure that this file is either small enough or is resized to meet our // requirements for attachments - const withBlob = await window.Signal.Util.AttachmentUtil.autoScale( - { - contentType: avatar.type, - file: new Blob([data.data], { - type: avatar.contentType, - }), - maxMeasurements: { - maxSize: 1000 * 1024, - maxHeight: 512, - maxWidth: 512, - }, - } - ); - const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile( - withBlob.file - ); - - // For simplicity we use the same attachment pointer that would send to - // others, which means we need to wait for the database response. - // To avoid the wait, we create a temporary url for the local image - // and use it until we the the response from the server - const tempUrl = window.URL.createObjectURL(avatar); - conversation.setLokiProfile({ displayName: newName }); - conversation.set('avatar', tempUrl); - - // Encrypt with a new key every time - profileKey = libsignal.crypto.getRandomBytes(32); - const encryptedData = await textsecure.crypto.encryptProfile( - dataResized, - profileKey - ); - - const avatarPointer = await libsession.Utils.AttachmentUtils.uploadAvatar( - { - ...dataResized, - data: encryptedData, - size: encryptedData.byteLength, - } - ); - - ({ url } = avatarPointer); - - storage.put('profileKey', profileKey); - - conversation.set('avatarPointer', url); - - const upgraded = await Signal.Migrations.processNewAttachment({ - isRaw: true, - data: data.data, - url, - }); - newAvatarPath = upgraded.path; - // Replace our temporary image with the attachment pointer from the server: - conversation.set('avatar', null); - conversation.setLokiProfile({ - displayName: newName, - avatar: newAvatarPath, - }); + try { + const withBlob = await window.Signal.Util.AttachmentUtil.autoScale( + { + contentType: avatar.type, + file: new Blob([data.data], { + type: avatar.contentType, + }), + maxMeasurements: { + maxSize: 1000 * 1024, // 1Mb for our profile picture + }, + } + ); + const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile( + withBlob.file + ); + + // For simplicity we use the same attachment pointer that would send to + // others, which means we need to wait for the database response. + // To avoid the wait, we create a temporary url for the local image + // and use it until we the the response from the server + const tempUrl = window.URL.createObjectURL(avatar); + conversation.setLokiProfile({ displayName: newName }); + conversation.set('avatar', tempUrl); + + // Encrypt with a new key every time + profileKey = libsignal.crypto.getRandomBytes(32); + const encryptedData = await textsecure.crypto.encryptProfile( + dataResized, + profileKey + ); + + const avatarPointer = await libsession.Utils.AttachmentUtils.uploadAvatar( + { + ...dataResized, + data: encryptedData, + size: encryptedData.byteLength, + } + ); + + ({ url } = avatarPointer); + + storage.put('profileKey', profileKey); + + conversation.set('avatarPointer', url); + + const upgraded = await Signal.Migrations.processNewAttachment({ + isRaw: true, + data: data.data, + url, + }); + newAvatarPath = upgraded.path; + // Replace our temporary image with the attachment pointer from the server: + conversation.set('avatar', null); + conversation.setLokiProfile({ + displayName: newName, + avatar: newAvatarPath, + }); + } catch (error) { + window.log.error( + 'showEditProfileDialog Error ensuring that image is properly sized:', + error && error.stack ? error.stack : error + ); + } } else { // do not update the avatar if it did not change conversation.setLokiProfile({ diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 9b6fdf714..36b0abb5a 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -805,11 +805,16 @@ export class SessionCompositionBox extends React.Component { } } - // this function is called right before sending a message, to gather really files bejind attachments. + // this function is called right before sending a message, to gather really the files behind attachments. private async getFiles() { const { stagedAttachments } = this.props; + // scale them down const files = await Promise.all( - stagedAttachments.map(attachment => AttachmentUtil.getFile(attachment)) + stagedAttachments.map(attachment => + AttachmentUtil.getFile(attachment, { + maxSize: Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES, + }) + ) ); this.props.clearAttachments(); return files; diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index e32b3a1a2..44244db5f 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -1058,6 +1058,7 @@ export class SessionConversation extends React.Component { } const url = await window.autoOrientImage(file); + this.addAttachments([ { file, @@ -1070,42 +1071,22 @@ export class SessionConversation extends React.Component { ]); }; + let blob = null; + try { - const blob = await AttachmentUtil.autoScale({ + blob = await AttachmentUtil.autoScale({ contentType, file, }); - let limitKb = 10000; - const blobType = - file.type === 'image/gif' ? 'gif' : contentType.split('/')[0]; - - switch (blobType) { - case 'image': - limitKb = 6000; - break; - case 'gif': - limitKb = 10000; - break; - case 'audio': - limitKb = 10000; - break; - case 'video': - limitKb = 10000; - break; - default: - limitKb = 10000; + + if ( + blob.file.size >= Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES + ) { + ToastUtils.pushFileSizeErrorAsByte( + Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES + ); + return; } - // if ((blob.file.size / 1024).toFixed(4) >= limitKb) { - // const units = ['kB', 'MB', 'GB']; - // let u = -1; - // let limit = limitKb * 1000; - // do { - // limit /= 1000; - // u += 1; - // } while (limit >= 1000 && u < units.length - 1); - // // this.showFileSizeError(limit, units[u]); - // return; - // } } catch (error) { window.log.error( 'Error ensuring that image is properly sized:', @@ -1118,6 +1099,10 @@ export class SessionConversation extends React.Component { try { if (GoogleChrome.isImageTypeSupported(contentType)) { + // this does not add the preview to the message outgoing + // this is just for us, for the list of attachments we are sending + // the files are scaled down under getFiles() + await renderImagePreview(); } else if (GoogleChrome.isVideoTypeSupported(contentType)) { await renderVideoPreview(); diff --git a/ts/components/session/conversation/SessionRecording.tsx b/ts/components/session/conversation/SessionRecording.tsx index dbaad9c73..a85927625 100644 --- a/ts/components/session/conversation/SessionRecording.tsx +++ b/ts/components/session/conversation/SessionRecording.tsx @@ -11,6 +11,7 @@ import { SessionButtonType, } from '../SessionButton'; import { Constants } from '../../../session'; +import { ToastUtils } from '../../../session/utils'; interface Props { onExitVoiceNoteView: any; @@ -422,8 +423,10 @@ export class SessionRecording extends React.Component { } // Is the audio file > attachment filesize limit - if (audioBlob.size > Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE) { - // TODO VINCE: warn the user that it's too big + if (audioBlob.size > Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES) { + ToastUtils.pushFileSizeErrorAsByte( + Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES + ); return; } diff --git a/ts/components/session/conversation/SessionStagedLinkPreview.tsx b/ts/components/session/conversation/SessionStagedLinkPreview.tsx index 2c1cde499..63c627657 100644 --- a/ts/components/session/conversation/SessionStagedLinkPreview.tsx +++ b/ts/components/session/conversation/SessionStagedLinkPreview.tsx @@ -62,12 +62,15 @@ 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, - file: new Blob([fullSizeImage.data], { - type: fullSizeImage.contentType, - }), - }); + const withBlob = await AttachmentUtil.autoScale( + { + contentType: fullSizeImage.contentType, + file: new Blob([fullSizeImage.data], { + type: fullSizeImage.contentType, + }), + }, + { maxSize: 100 * 1000 } // this is a preview image. No need for it to be crazy big. 100k is big enough + ); const data = await arrayBufferFromFile(withBlob.file); objectUrl = URL.createObjectURL(withBlob.file); diff --git a/ts/session/constants.ts b/ts/session/constants.ts index 1beed79a2..a80fb08c8 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -28,8 +28,8 @@ export const CONVERSATION = { // Maximum voice message duraton of 5 minutes // which equates to 1.97 MB MAX_VOICE_MESSAGE_DURATION: 300, - // Max attachment size: 10 MB - MAX_ATTACHMENT_FILESIZE: 10000000, + // Max attachment size: 6 MB + MAX_ATTACHMENT_FILESIZE_BYTES: 6 * 1000 * 1000, }; export const UI = { diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 6e37ccd59..89722397d 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -80,6 +80,17 @@ export function pushFileSizeError(limit: number, units: string) { ); } +export function pushFileSizeErrorAsByte(bytesCount: number) { + const units = ['kB', 'MB', 'GB']; + let u = -1; + let limit = bytesCount; + do { + limit /= 1000; + u += 1; + } while (limit >= 1000 && u < units.length - 1); + pushFileSizeError(limit, units[u]); +} + export function pushMultipleNonImageError() { pushToastError( 'cannotMixImageAndNonImageAttachments', diff --git a/ts/util/attachmentsUtil.ts b/ts/util/attachmentsUtil.ts index 58b861176..6f5268ba6 100644 --- a/ts/util/attachmentsUtil.ts +++ b/ts/util/attachmentsUtil.ts @@ -1,16 +1,18 @@ import { StagedAttachmentType } from '../components/session/conversation/SessionCompositionBox'; import { SignalService } from '../protobuf'; +import { Constants } from '../session'; export interface MaxScaleSize { - maxSize: number; - maxHeight: number; - maxWidth: number; + maxSize?: number; + maxHeight?: number; + maxWidth?: number; } -export async function autoScale< - T extends { contentType: string; file: any; maxMeasurements?: MaxScaleSize } ->(attachment: T): Promise { - const { contentType, file, maxMeasurements } = attachment; +export async function autoScale( + attachment: T, + maxMeasurements?: MaxScaleSize +): Promise { + const { contentType, file } = attachment; if (contentType.split('/')[0] !== 'image' || contentType === 'image/tiff') { // nothing to do return Promise.resolve(attachment); @@ -23,7 +25,9 @@ export async function autoScale< img.onload = () => { URL.revokeObjectURL(url); - const maxSize = maxMeasurements?.maxSize || 6000 * 1024; + const maxSize = + maxMeasurements?.maxSize || + Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES; const maxHeight = maxMeasurements?.maxHeight || 4096; const maxWidth = maxMeasurements?.maxWidth || 4096; @@ -36,7 +40,7 @@ export async function autoScale< return; } - const gifMaxSize = 25000 * 1024; + const gifMaxSize = Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES; if (file.type === 'image/gif' && file.size <= gifMaxSize) { resolve(attachment); return; @@ -62,11 +66,15 @@ export async function autoScale< canvas.toDataURL('image/jpeg', quality) ); quality = (quality * maxSize) / blob.size; + // Should we disallow the algo drop the quality too low? + // if (quality < 0.5) { + // quality = 0.5; + // } // NOTE: During testing with a large image, we observed the // `quality` value being > 1. Should we clamp it to [0.5, 1.0]? // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax - if (quality < 0.5) { - quality = 0.5; + if (quality > 1) { + quality = 1; } } while (i > 0 && blob.size > maxSize); @@ -79,7 +87,10 @@ export async function autoScale< }); } -export async function getFile(attachment: StagedAttachmentType) { +export async function getFile( + attachment: StagedAttachmentType, + maxMeasurements?: MaxScaleSize +) { if (!attachment) { return Promise.resolve(); } @@ -88,7 +99,7 @@ export async function getFile(attachment: StagedAttachmentType) { ? SignalService.AttachmentPointer.Flags.VOICE_MESSAGE : null; - const scaled = await autoScale(attachment); + const scaled = await autoScale(attachment, maxMeasurements); const fileRead = await readFile(scaled); return { ...fileRead,