New staged attachments UI, multiple image attachments per message
This commit is contained in:
parent
e4babdaef0
commit
985b1d6aa6
22 changed files with 1550 additions and 648 deletions
|
@ -30,91 +30,405 @@
|
|||
},
|
||||
});
|
||||
|
||||
Whisper.UnsupportedFileTypeToast = Whisper.ToastView.extend({
|
||||
template: i18n('unsupportedFileType'),
|
||||
});
|
||||
|
||||
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(options) {
|
||||
this.$input = this.$('input[type=file]');
|
||||
this.$input.click(e => {
|
||||
e.stopPropagation();
|
||||
initialize() {
|
||||
this.attachments = [];
|
||||
|
||||
this.attachmentListView = new Whisper.ReactWrapperView({
|
||||
el: this.el,
|
||||
Component: window.Signal.Components.AttachmentList,
|
||||
props: this.getPropsForAttachmentList(),
|
||||
});
|
||||
this.thumb = new Whisper.AttachmentPreviewView();
|
||||
this.$el.addClass('file-input');
|
||||
this.window = options.window;
|
||||
this.previewObjectUrl = null;
|
||||
},
|
||||
|
||||
events: {
|
||||
'change .choose-file': 'previewImages',
|
||||
'click .close': 'deleteFiles',
|
||||
'click .choose-file': 'open',
|
||||
drop: 'openDropped',
|
||||
dragover: 'showArea',
|
||||
dragleave: 'hideArea',
|
||||
paste: 'onPaste',
|
||||
remove() {
|
||||
if (this.attachmentListView) {
|
||||
this.attachmentListView.remove();
|
||||
}
|
||||
if (this.captionEditorView) {
|
||||
this.captionEditorView.remove();
|
||||
}
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
open(e) {
|
||||
render() {
|
||||
this.attachmentListView.update(this.getPropsForAttachmentList());
|
||||
},
|
||||
|
||||
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,
|
||||
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,
|
||||
onChangeCaption,
|
||||
});
|
||||
|
||||
const update = () => {
|
||||
this.captionEditorView.update(getProps());
|
||||
};
|
||||
|
||||
const onChangeCaption = caption => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
attachment.caption = caption;
|
||||
this.render();
|
||||
update();
|
||||
};
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
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();
|
||||
// hack
|
||||
if (this.window && this.window.chrome && this.window.chrome.fileSystem) {
|
||||
this.window.chrome.fileSystem.chooseEntry(
|
||||
{ type: 'openFile' },
|
||||
entry => {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
entry.file(file => {
|
||||
this.file = file;
|
||||
this.previewImages();
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.$input.click();
|
||||
this.$el.addClass('dropoff');
|
||||
},
|
||||
|
||||
onDragLeave(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.$el.removeClass('dropoff');
|
||||
},
|
||||
|
||||
onDrop(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const file = e.originalEvent.dataTransfer.files[0];
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
addThumb(src, options = {}) {
|
||||
_.defaults(options, { addPlayIcon: false });
|
||||
this.$('.avatar').hide();
|
||||
this.thumb.src = src;
|
||||
this.$('.attachment-previews').append(this.thumb.render().el);
|
||||
// Public interface
|
||||
|
||||
if (options.addPlayIcon) {
|
||||
this.$el.addClass('video-attachment');
|
||||
} else {
|
||||
this.$el.removeClass('video-attachment');
|
||||
}
|
||||
|
||||
this.thumb.$('img')[0].onload = () => {
|
||||
this.$el.trigger('force-resize');
|
||||
};
|
||||
this.thumb.$('img')[0].onerror = () => {
|
||||
this.unableToLoadAttachment();
|
||||
};
|
||||
hasFiles() {
|
||||
return this.attachments.length > 0;
|
||||
},
|
||||
|
||||
unableToLoadAttachment() {
|
||||
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();
|
||||
|
||||
this.deleteFiles();
|
||||
},
|
||||
|
||||
autoScale(file) {
|
||||
if (file.type.split('/')[0] !== 'image' || file.type === 'image/tiff') {
|
||||
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.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(file);
|
||||
return Promise.resolve(attachment);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -132,13 +446,13 @@
|
|||
img.naturalHeight <= maxHeight &&
|
||||
file.size <= maxSize
|
||||
) {
|
||||
resolve(file);
|
||||
resolve(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
const gifMaxSize = 25000 * 1024;
|
||||
if (file.type === 'image/gif' && file.size <= gifMaxSize) {
|
||||
resolve(file);
|
||||
resolve(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -170,285 +484,47 @@
|
|||
}
|
||||
} while (i > 0 && blob.size > maxSize);
|
||||
|
||||
resolve(blob);
|
||||
resolve({
|
||||
...attachment,
|
||||
file: blob,
|
||||
});
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
},
|
||||
|
||||
async previewImages() {
|
||||
this.clearForm();
|
||||
const file = this.file || this.$input.prop('files')[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
const { name } = file;
|
||||
if (window.Signal.Util.isFileDangerous(name)) {
|
||||
this.deleteFiles();
|
||||
|
||||
const toast = new Whisper.DangerousFileTypeToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = file.type;
|
||||
|
||||
const renderVideoPreview = async () => {
|
||||
// we use the variable on this here to ensure cleanup if we're interrupted
|
||||
this.previewObjectUrl = URL.createObjectURL(file);
|
||||
const type = 'image/png';
|
||||
const thumbnail = await VisualAttachment.makeVideoThumbnail({
|
||||
size: 100,
|
||||
videoObjectUrl: this.previewObjectUrl,
|
||||
contentType: type,
|
||||
logger: window.log,
|
||||
});
|
||||
URL.revokeObjectURL(this.previewObjectUrl);
|
||||
|
||||
const data = await VisualAttachment.blobToArrayBuffer(thumbnail);
|
||||
this.previewObjectUrl = Signal.Util.arrayBufferToObjectURL({
|
||||
data,
|
||||
type,
|
||||
});
|
||||
this.addThumb(this.previewObjectUrl, { addPlayIcon: true });
|
||||
};
|
||||
|
||||
const renderImagePreview = async () => {
|
||||
if (!MIME.isJPEG(file.type)) {
|
||||
this.previewObjectUrl = URL.createObjectURL(file);
|
||||
if (!this.previewObjectUrl) {
|
||||
throw new Error('Failed to create object url for image!');
|
||||
}
|
||||
this.addThumb(this.previewObjectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataUrl = await window.autoOrientImage(file);
|
||||
this.addThumb(dataUrl);
|
||||
};
|
||||
|
||||
try {
|
||||
if (Signal.Util.GoogleChrome.isImageTypeSupported(contentType)) {
|
||||
await renderImagePreview();
|
||||
} else if (Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)) {
|
||||
await renderVideoPreview();
|
||||
} else if (MIME.isAudio(contentType)) {
|
||||
this.addThumb('images/audio.svg');
|
||||
} else {
|
||||
this.addThumb('images/file.svg');
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`Was unable to generate thumbnail for file type ${contentType}`,
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
this.addThumb('images/file.svg');
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await this.autoScale(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.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);
|
||||
const toast = new Whisper.FileSizeToast({
|
||||
model: { limit, units: units[u] },
|
||||
});
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
this.deleteFiles();
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error ensuring that image is properly sized:',
|
||||
error && error.message ? error.message : error
|
||||
);
|
||||
|
||||
this.unableToLoadAttachment();
|
||||
}
|
||||
},
|
||||
|
||||
hasFiles() {
|
||||
const files = this.file ? [this.file] : this.$input.prop('files');
|
||||
return files && files.length && files.length > 0;
|
||||
},
|
||||
|
||||
getFiles() {
|
||||
const files = this.file
|
||||
? [this.file]
|
||||
: Array.from(this.$input.prop('files'));
|
||||
const promise = Promise.all(files.map(file => this.getFile(file)));
|
||||
this.clearForm();
|
||||
return promise;
|
||||
},
|
||||
|
||||
getFile(rawFile) {
|
||||
const file = rawFile || this.file || this.$input.prop('files')[0];
|
||||
if (!file) {
|
||||
async getFile(attachment) {
|
||||
if (!attachment) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const attachmentFlags = this.isVoiceNote
|
||||
const attachmentFlags = attachment.isVoiceNote
|
||||
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||
: null;
|
||||
|
||||
const setFlags = flags => attachment => {
|
||||
const newAttachment = Object.assign({}, attachment);
|
||||
if (flags) {
|
||||
newAttachment.flags = flags;
|
||||
}
|
||||
return newAttachment;
|
||||
const scaled = await this.autoScale(attachment);
|
||||
const fileRead = await this.readFile(scaled);
|
||||
return {
|
||||
...fileRead,
|
||||
url: undefined,
|
||||
videoUrl: undefined,
|
||||
flags: attachmentFlags || null,
|
||||
};
|
||||
|
||||
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||
// to `async` / `await`:
|
||||
// eslint-disable-next-line more/no-then
|
||||
return this.autoScale(file)
|
||||
.then(this.readFile)
|
||||
.then(setFlags(attachmentFlags));
|
||||
},
|
||||
|
||||
async getThumbnail() {
|
||||
// Scale and crop an image to 256px square
|
||||
const size = 256;
|
||||
const file = this.file || this.$input.prop('files')[0];
|
||||
if (
|
||||
file === undefined ||
|
||||
file.type.split('/')[0] !== 'image' ||
|
||||
file.type === 'image/gif'
|
||||
) {
|
||||
// nothing to do
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
const arrayBuffer = await VisualAttachment.makeImageThumbnail({
|
||||
size,
|
||||
objectUrl,
|
||||
logger: window.log,
|
||||
});
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
|
||||
return this.readFile(arrayBuffer);
|
||||
},
|
||||
|
||||
// File -> Promise Attachment
|
||||
readFile(file) {
|
||||
readFile(attachment) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const FR = new FileReader();
|
||||
FR.onload = e => {
|
||||
resolve({
|
||||
...attachment,
|
||||
data: e.target.result,
|
||||
contentType: file.type,
|
||||
fileName: file.name,
|
||||
size: file.size,
|
||||
});
|
||||
};
|
||||
FR.onerror = reject;
|
||||
FR.onabort = reject;
|
||||
FR.readAsArrayBuffer(file);
|
||||
FR.readAsArrayBuffer(attachment.file);
|
||||
});
|
||||
},
|
||||
|
||||
clearForm() {
|
||||
if (this.previewObjectUrl) {
|
||||
URL.revokeObjectURL(this.previewObjectUrl);
|
||||
this.previewObjectUrl = null;
|
||||
}
|
||||
|
||||
this.thumb.remove();
|
||||
this.$('.avatar').show();
|
||||
this.$el.trigger('force-resize');
|
||||
},
|
||||
|
||||
deleteFiles(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
this.clearForm();
|
||||
this.$input
|
||||
.wrap('<form>')
|
||||
.parent('form')
|
||||
.trigger('reset');
|
||||
this.$input.unwrap();
|
||||
this.file = null;
|
||||
this.$input.trigger('change');
|
||||
this.isVoiceNote = false;
|
||||
},
|
||||
|
||||
openDropped(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.file = e.originalEvent.dataTransfer.files[0];
|
||||
this.previewImages();
|
||||
this.$el.removeClass('dropoff');
|
||||
},
|
||||
|
||||
showArea(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.$el.addClass('dropoff');
|
||||
},
|
||||
|
||||
hideArea(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
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) {
|
||||
this.file = imgBlob;
|
||||
this.previewImages();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue