Persist drafts
This commit is contained in:
parent
5ebd8bc690
commit
9d4f2afa5a
23 changed files with 1048 additions and 720 deletions
|
@ -150,6 +150,34 @@
|
|||
return this.id === this.ourNumber;
|
||||
},
|
||||
|
||||
hasDraft() {
|
||||
const draftAttachments = this.get('draftAttachments') || [];
|
||||
return (
|
||||
this.get('draft') ||
|
||||
this.get('quotedMessageId') ||
|
||||
draftAttachments.length > 0
|
||||
);
|
||||
},
|
||||
|
||||
getDraftPreview() {
|
||||
const draft = this.get('draft');
|
||||
if (draft) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const draftAttachments = this.get('draftAttachments') || [];
|
||||
if (draftAttachments.length > 0) {
|
||||
return i18n('Conversation--getDraftPreview--attachment');
|
||||
}
|
||||
|
||||
const quotedMessageId = this.get('quotedMessageId');
|
||||
if (quotedMessageId) {
|
||||
return i18n('Conversation--getDraftPreview--quote');
|
||||
}
|
||||
|
||||
return i18n('Conversation--getDraftPreview--draft');
|
||||
},
|
||||
|
||||
bumpTyping() {
|
||||
// We don't send typing messages if the setting is disabled
|
||||
if (!storage.get('typingIndicators')) {
|
||||
|
@ -327,6 +355,13 @@
|
|||
? ConversationController.getOrCreate(typingMostRecent.sender, 'private')
|
||||
: null;
|
||||
|
||||
const timestamp = this.get('timestamp');
|
||||
const draftTimestamp = this.get('draftTimestamp');
|
||||
const draftPreview = this.getDraftPreview();
|
||||
const draftText = this.get('draft');
|
||||
const shouldShowDraft =
|
||||
this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp;
|
||||
|
||||
const result = {
|
||||
id: this.id,
|
||||
|
||||
|
@ -340,10 +375,14 @@
|
|||
lastUpdated: this.get('timestamp'),
|
||||
name: this.getName(),
|
||||
profileName: this.getProfileName(),
|
||||
timestamp: this.get('timestamp'),
|
||||
timestamp,
|
||||
title: this.getTitle(),
|
||||
unreadCount: this.get('unreadCount') || 0,
|
||||
|
||||
shouldShowDraft,
|
||||
draftPreview,
|
||||
draftText,
|
||||
|
||||
phoneNumber: format(this.id, {
|
||||
ourRegionCode: regionCode,
|
||||
}),
|
||||
|
@ -970,6 +1009,8 @@
|
|||
active_at: now,
|
||||
timestamp: now,
|
||||
isArchived: false,
|
||||
draft: null,
|
||||
draftTimestamp: null,
|
||||
});
|
||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
|
@ -1226,6 +1267,15 @@
|
|||
);
|
||||
|
||||
const lastMessageModel = messages.at(0);
|
||||
if (
|
||||
this.hasDraft() &&
|
||||
this.get('draftTimestamp') &&
|
||||
(!lastMessageModel ||
|
||||
lastMessageModel.get('sent_at') < this.get('draftTimestamp'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMessageJSON = lastMessageModel
|
||||
? lastMessageModel.toJSON()
|
||||
: null;
|
||||
|
|
|
@ -9,7 +9,6 @@ const {
|
|||
isFunction,
|
||||
isObject,
|
||||
map,
|
||||
merge,
|
||||
set,
|
||||
} = require('lodash');
|
||||
|
||||
|
@ -29,6 +28,7 @@ const ERASE_SQL_KEY = 'erase-sql-key';
|
|||
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||
const ERASE_STICKERS_KEY = 'erase-stickers';
|
||||
const ERASE_TEMP_KEY = 'erase-temp';
|
||||
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||
|
||||
const _jobs = Object.create(null);
|
||||
|
@ -598,7 +598,10 @@ async function updateConversation(id, data, { Conversation }) {
|
|||
throw new Error(`Conversation ${id} does not exist!`);
|
||||
}
|
||||
|
||||
const merged = merge({}, existing.attributes, data);
|
||||
const merged = {
|
||||
...existing.attributes,
|
||||
...data,
|
||||
};
|
||||
await channels.updateConversation(merged);
|
||||
}
|
||||
|
||||
|
@ -1007,6 +1010,7 @@ async function removeOtherData() {
|
|||
callChannel(ERASE_ATTACHMENTS_KEY),
|
||||
callChannel(ERASE_STICKERS_KEY),
|
||||
callChannel(ERASE_TEMP_KEY),
|
||||
callChannel(ERASE_DRAFTS_KEY),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -103,20 +103,21 @@ function initializeMigrations({
|
|||
return null;
|
||||
}
|
||||
const {
|
||||
createAbsolutePathGetter,
|
||||
createReader,
|
||||
createWriterForExisting,
|
||||
createWriterForNew,
|
||||
getDraftPath,
|
||||
getPath,
|
||||
getStickersPath,
|
||||
getTempPath,
|
||||
createReader,
|
||||
createAbsolutePathGetter,
|
||||
createWriterForNew,
|
||||
createWriterForExisting,
|
||||
} = Attachments;
|
||||
const {
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions,
|
||||
makeImageThumbnail,
|
||||
makeObjectUrl,
|
||||
makeVideoScreenshot,
|
||||
revokeObjectUrl,
|
||||
} = VisualType;
|
||||
|
||||
const attachmentsPath = getPath(userDataPath);
|
||||
|
@ -147,11 +148,18 @@ function initializeMigrations({
|
|||
tempPath
|
||||
);
|
||||
|
||||
const draftPath = getDraftPath(userDataPath);
|
||||
const getAbsoluteDraftPath = createAbsolutePathGetter(draftPath);
|
||||
const writeNewDraftData = createWriterForNew(draftPath);
|
||||
const deleteDraftFile = Attachments.createDeleter(draftPath);
|
||||
const readDraftData = createReader(draftPath);
|
||||
|
||||
return {
|
||||
attachmentsPath,
|
||||
copyIntoAttachmentsDirectory,
|
||||
copyIntoTempDirectory,
|
||||
deleteAttachmentData: deleteOnDisk,
|
||||
deleteDraftFile,
|
||||
deleteExternalMessageFiles: MessageType.deleteAllExternalFiles({
|
||||
deleteAttachmentData: Type.deleteData(deleteOnDisk),
|
||||
deleteOnDisk,
|
||||
|
@ -159,6 +167,7 @@ function initializeMigrations({
|
|||
deleteSticker,
|
||||
deleteTempFile,
|
||||
getAbsoluteAttachmentPath,
|
||||
getAbsoluteDraftPath,
|
||||
getAbsoluteStickerPath,
|
||||
getAbsoluteTempPath,
|
||||
getPlaceholderMigrations,
|
||||
|
@ -169,6 +178,7 @@ function initializeMigrations({
|
|||
loadQuoteData,
|
||||
loadStickerData,
|
||||
readAttachmentData,
|
||||
readDraftData,
|
||||
readStickerData,
|
||||
readTempData,
|
||||
run,
|
||||
|
@ -218,6 +228,7 @@ function initializeMigrations({
|
|||
logger,
|
||||
}),
|
||||
writeNewAttachmentData: createWriterForNew(attachmentsPath),
|
||||
writeNewDraftData,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,575 +0,0 @@
|
|||
/* global textsecure: false */
|
||||
/* global Whisper: false */
|
||||
/* global i18n: false */
|
||||
/* global loadImage: false */
|
||||
/* global Backbone: false */
|
||||
/* global _: false */
|
||||
/* global Signal: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { MIME, VisualAttachment } = window.Signal.Types;
|
||||
|
||||
Whisper.FileSizeToast = Whisper.ToastView.extend({
|
||||
templateName: 'file-size-modal',
|
||||
render_attributes() {
|
||||
return {
|
||||
'file-size-warning': i18n('fileSizeWarning'),
|
||||
limit: this.model.limit,
|
||||
units: this.model.units,
|
||||
};
|
||||
},
|
||||
});
|
||||
Whisper.UnableToLoadToast = Whisper.ToastView.extend({
|
||||
render_attributes() {
|
||||
return { toastMessage: i18n('unableToLoadAttachment') };
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({
|
||||
template: i18n('dangerousFileType'),
|
||||
});
|
||||
Whisper.OneNonImageAtATimeToast = Whisper.ToastView.extend({
|
||||
template: i18n('oneNonImageAtATimeToast'),
|
||||
});
|
||||
Whisper.CannotMixImageAndNonImageAttachmentsToast = Whisper.ToastView.extend({
|
||||
template: i18n('cannotMixImageAdnNonImageAttachments'),
|
||||
});
|
||||
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
||||
template: i18n('maximumAttachments'),
|
||||
});
|
||||
|
||||
Whisper.FileInputView = Backbone.View.extend({
|
||||
tagName: 'span',
|
||||
className: 'file-input',
|
||||
initialize() {
|
||||
this.attachments = [];
|
||||
|
||||
this.attachmentListView = new Whisper.ReactWrapperView({
|
||||
el: this.el,
|
||||
Component: window.Signal.Components.AttachmentList,
|
||||
props: this.getPropsForAttachmentList(),
|
||||
});
|
||||
},
|
||||
|
||||
remove() {
|
||||
if (this.attachmentListView) {
|
||||
this.attachmentListView.remove();
|
||||
}
|
||||
if (this.captionEditorView) {
|
||||
this.captionEditorView.remove();
|
||||
}
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
render() {
|
||||
this.attachmentListView.update(this.getPropsForAttachmentList());
|
||||
this.trigger('staged-attachments-changed');
|
||||
},
|
||||
|
||||
getPropsForAttachmentList() {
|
||||
const { attachments } = this;
|
||||
|
||||
// We never want to display voice notes in our attachment list
|
||||
if (_.any(attachments, attachment => Boolean(attachment.isVoiceNote))) {
|
||||
return {
|
||||
attachments: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
attachments,
|
||||
onAddAttachment: this.onAddAttachment.bind(this),
|
||||
onClickAttachment: this.onClickAttachment.bind(this),
|
||||
onCloseAttachment: this.onCloseAttachment.bind(this),
|
||||
onClose: this.onClose.bind(this),
|
||||
};
|
||||
},
|
||||
|
||||
onClickAttachment(attachment) {
|
||||
const getProps = () => ({
|
||||
url: attachment.videoUrl || attachment.url,
|
||||
caption: attachment.caption,
|
||||
attachment,
|
||||
onSave,
|
||||
});
|
||||
|
||||
const onSave = caption => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
attachment.caption = caption;
|
||||
this.captionEditorView.remove();
|
||||
Signal.Backbone.Views.Lightbox.hide();
|
||||
this.render();
|
||||
};
|
||||
|
||||
this.captionEditorView = new Whisper.ReactWrapperView({
|
||||
className: 'attachment-list-wrapper',
|
||||
Component: window.Signal.Components.CaptionEditor,
|
||||
props: getProps(),
|
||||
onClose: () => Signal.Backbone.Views.Lightbox.hide(),
|
||||
});
|
||||
Signal.Backbone.Views.Lightbox.show(this.captionEditorView.el);
|
||||
},
|
||||
|
||||
onCloseAttachment(attachment) {
|
||||
this.attachments = _.without(this.attachments, attachment);
|
||||
this.render();
|
||||
},
|
||||
|
||||
onAddAttachment() {
|
||||
this.trigger('choose-attachment');
|
||||
},
|
||||
|
||||
onClose() {
|
||||
this.attachments = [];
|
||||
this.render();
|
||||
},
|
||||
|
||||
// These event handlers are called by ConversationView, which listens for these events
|
||||
|
||||
onDragOver(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.$el.addClass('dropoff');
|
||||
},
|
||||
|
||||
onDragLeave(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.$el.removeClass('dropoff');
|
||||
},
|
||||
|
||||
async onDrop(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const { files } = e.originalEvent.dataTransfer;
|
||||
for (let i = 0, max = files.length; i < max; i += 1) {
|
||||
const file = files[i];
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.maybeAddAttachment(file);
|
||||
}
|
||||
|
||||
this.$el.removeClass('dropoff');
|
||||
},
|
||||
|
||||
onPaste(e) {
|
||||
const { items } = e.originalEvent.clipboardData;
|
||||
let imgBlob = null;
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
if (items[i].type.split('/')[0] === 'image') {
|
||||
imgBlob = items[i].getAsFile();
|
||||
}
|
||||
}
|
||||
if (imgBlob !== null) {
|
||||
const file = imgBlob;
|
||||
this.maybeAddAttachment(file);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
// Public interface
|
||||
|
||||
hasFiles() {
|
||||
return this.attachments.length > 0;
|
||||
},
|
||||
|
||||
async getFiles() {
|
||||
const files = await Promise.all(
|
||||
this.attachments.map(attachment => this.getFile(attachment))
|
||||
);
|
||||
this.clearAttachments();
|
||||
return files;
|
||||
},
|
||||
|
||||
clearAttachments() {
|
||||
this.attachments.forEach(attachment => {
|
||||
if (attachment.url) {
|
||||
URL.revokeObjectURL(attachment.url);
|
||||
}
|
||||
if (attachment.videoUrl) {
|
||||
URL.revokeObjectURL(attachment.videoUrl);
|
||||
}
|
||||
});
|
||||
|
||||
this.attachments = [];
|
||||
this.render();
|
||||
this.$el.trigger('force-resize');
|
||||
},
|
||||
|
||||
// Show errors
|
||||
|
||||
showLoadFailure() {
|
||||
const toast = new Whisper.UnableToLoadToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showDangerousError() {
|
||||
const toast = new Whisper.DangerousFileTypeToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showFileSizeError({ limit, units, u }) {
|
||||
const toast = new Whisper.FileSizeToast({
|
||||
model: { limit, units: units[u] },
|
||||
});
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showCannotMixError() {
|
||||
const toast = new Whisper.CannotMixImageAndNonImageAttachmentsToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showMultipleNonImageError() {
|
||||
const toast = new Whisper.OneNonImageAtATimeToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showMaximumAttachmentsError() {
|
||||
const toast = new Whisper.MaxAttachmentsToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
// Housekeeping
|
||||
|
||||
addAttachment(attachment) {
|
||||
if (attachment.isVoiceNote && this.attachments.length > 0) {
|
||||
throw new Error('A voice note cannot be sent with other attachments');
|
||||
}
|
||||
|
||||
this.attachments.push(attachment);
|
||||
this.render();
|
||||
},
|
||||
|
||||
async maybeAddAttachment(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = file.name;
|
||||
const contentType = file.type;
|
||||
|
||||
if (window.Signal.Util.isFileDangerous(fileName)) {
|
||||
this.showDangerousError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.attachments.length >= 32) {
|
||||
this.showMaximumAttachmentsError();
|
||||
return;
|
||||
}
|
||||
|
||||
const haveNonImage = _.any(
|
||||
this.attachments,
|
||||
attachment => !MIME.isImage(attachment.contentType)
|
||||
);
|
||||
// You can't add another attachment if you already have a non-image staged
|
||||
if (haveNonImage) {
|
||||
this.showMultipleNonImageError();
|
||||
return;
|
||||
}
|
||||
|
||||
// You can't add a non-image attachment if you already have attachments staged
|
||||
if (!MIME.isImage(contentType) && this.attachments.length > 0) {
|
||||
this.showCannotMixError();
|
||||
return;
|
||||
}
|
||||
|
||||
const renderVideoPreview = async () => {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
try {
|
||||
const type = 'image/png';
|
||||
const thumbnail = await VisualAttachment.makeVideoScreenshot({
|
||||
objectUrl,
|
||||
contentType: type,
|
||||
logger: window.log,
|
||||
});
|
||||
const data = await VisualAttachment.blobToArrayBuffer(thumbnail);
|
||||
const url = Signal.Util.arrayBufferToObjectURL({
|
||||
data,
|
||||
type,
|
||||
});
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
fileName,
|
||||
contentType,
|
||||
videoUrl: objectUrl,
|
||||
url,
|
||||
});
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const renderImagePreview = async () => {
|
||||
if (!MIME.isJPEG(contentType)) {
|
||||
const url = URL.createObjectURL(file);
|
||||
if (!url) {
|
||||
throw new Error('Failed to create object url for image!');
|
||||
}
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
fileName,
|
||||
contentType,
|
||||
url,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const url = await window.autoOrientImage(file);
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
fileName,
|
||||
contentType,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const blob = await this.autoScale({
|
||||
contentType,
|
||||
file,
|
||||
});
|
||||
let limitKb = 1000000;
|
||||
const blobType =
|
||||
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
|
||||
|
||||
switch (blobType) {
|
||||
case 'image':
|
||||
limitKb = 6000;
|
||||
break;
|
||||
case 'gif':
|
||||
limitKb = 25000;
|
||||
break;
|
||||
case 'audio':
|
||||
limitKb = 100000;
|
||||
break;
|
||||
case 'video':
|
||||
limitKb = 100000;
|
||||
break;
|
||||
default:
|
||||
limitKb = 100000;
|
||||
break;
|
||||
}
|
||||
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:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
||||
this.showLoadFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Signal.Util.GoogleChrome.isImageTypeSupported(contentType)) {
|
||||
await renderImagePreview();
|
||||
} else if (Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)) {
|
||||
await renderVideoPreview();
|
||||
} else {
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
contentType,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`Was unable to generate thumbnail for file type ${contentType}`,
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
contentType,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
autoScale(attachment) {
|
||||
const { contentType, file } = attachment;
|
||||
if (
|
||||
contentType.split('/')[0] !== 'image' ||
|
||||
contentType === 'image/tiff'
|
||||
) {
|
||||
// nothing to do
|
||||
return Promise.resolve(attachment);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = document.createElement('img');
|
||||
img.onerror = reject;
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
const maxSize = 6000 * 1024;
|
||||
const maxHeight = 4096;
|
||||
const maxWidth = 4096;
|
||||
if (
|
||||
img.naturalWidth <= maxWidth &&
|
||||
img.naturalHeight <= maxHeight &&
|
||||
file.size <= maxSize
|
||||
) {
|
||||
resolve(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
const gifMaxSize = 25000 * 1024;
|
||||
if (file.type === 'image/gif' && file.size <= gifMaxSize) {
|
||||
resolve(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.type === 'image/gif') {
|
||||
reject(new Error('GIF is too large'));
|
||||
return;
|
||||
}
|
||||
|
||||
const targetContentType = 'image/jpeg';
|
||||
const canvas = loadImage.scale(img, {
|
||||
canvas: true,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
});
|
||||
|
||||
let quality = 0.95;
|
||||
let i = 4;
|
||||
let blob;
|
||||
do {
|
||||
i -= 1;
|
||||
blob = window.dataURLToBlobSync(
|
||||
canvas.toDataURL(targetContentType, quality)
|
||||
);
|
||||
quality = quality * maxSize / blob.size;
|
||||
// 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;
|
||||
}
|
||||
} while (i > 0 && blob.size > maxSize);
|
||||
|
||||
resolve({
|
||||
...attachment,
|
||||
fileName: this.fixExtension(attachment.fileName, targetContentType),
|
||||
contentType: targetContentType,
|
||||
file: blob,
|
||||
});
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
},
|
||||
|
||||
getFileName(fileName) {
|
||||
if (!fileName) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!fileName.includes('.')) {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
return fileName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
},
|
||||
|
||||
getType(contentType) {
|
||||
if (!contentType) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!contentType.includes('/')) {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
return contentType.split('/')[1];
|
||||
},
|
||||
|
||||
fixExtension(fileName, contentType) {
|
||||
const extension = this.getType(contentType);
|
||||
const name = this.getFileName(fileName);
|
||||
return `${name}.${extension}`;
|
||||
},
|
||||
|
||||
async getFile(attachment) {
|
||||
if (!attachment) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const attachmentFlags = attachment.isVoiceNote
|
||||
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||
: null;
|
||||
|
||||
const scaled = await this.autoScale(attachment);
|
||||
const fileRead = await this.readFile(scaled);
|
||||
return {
|
||||
...fileRead,
|
||||
url: undefined,
|
||||
videoUrl: undefined,
|
||||
flags: attachmentFlags || null,
|
||||
};
|
||||
},
|
||||
|
||||
readFile(attachment) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const FR = new FileReader();
|
||||
FR.onload = e => {
|
||||
const data = e.target.result;
|
||||
resolve({
|
||||
...attachment,
|
||||
data,
|
||||
size: data.byteLength,
|
||||
});
|
||||
};
|
||||
FR.onerror = reject;
|
||||
FR.onabort = reject;
|
||||
FR.readAsArrayBuffer(attachment.file);
|
||||
});
|
||||
},
|
||||
});
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue