New staged attachments UI, multiple image attachments per message

This commit is contained in:
Scott Nonnenberg 2018-12-01 17:48:53 -08:00
parent e4babdaef0
commit 985b1d6aa6
22 changed files with 1550 additions and 648 deletions

View file

@ -172,6 +172,10 @@
"message": "Choose folder", "message": "Choose folder",
"description": "Button to allow the user to find a folder on disk" "description": "Button to allow the user to find a folder on disk"
}, },
"chooseFile": {
"message": "Choose file",
"description": "Button to allow the user to find a file on disk"
},
"loadDataHeader": { "loadDataHeader": {
"message": "Load your data", "message": "Load your data",
"description": "Header shown on the first screen in the data import process" "description": "Header shown on the first screen in the data import process"
@ -542,15 +546,27 @@
"message": "Voice Message", "message": "Voice Message",
"description": "Name for a voice message attachment" "description": "Name for a voice message attachment"
}, },
"unsupportedFileType": {
"message": "Unsupported file type",
"description": "Displayed for outgoing unsupported attachment"
},
"dangerousFileType": { "dangerousFileType": {
"message": "Attachment type not allowed for security reasons", "message": "Attachment type not allowed for security reasons",
"description": "description":
"Shown in toast when user attempts to send .exe file, for example" "Shown in toast when user attempts to send .exe file, for example"
}, },
"oneNonImageAtATimeToast": {
"message":
"When including a non-image attachment, the limit is one attachment per message.",
"description":
"An error popup when the user has attempted to add an attachment"
},
"cannotMixImageAdnNonImageAttachments": {
"message": "You cannot mix non-image and image attachments in one message.",
"description":
"An error popup when the user has attempted to add an attachment"
},
"maximumAttachments": {
"message": "You cannot add any more attachments to this message.",
"description":
"An error popup when the user has attempted to add an attachment"
},
"fileSizeWarning": { "fileSizeWarning": {
"message": "Sorry, the selected file exceeds message size restrictions." "message": "Sorry, the selected file exceeds message size restrictions."
}, },
@ -732,6 +748,12 @@
"description": "description":
"Shown in toast if user clicks on quote references messages not loaded in view, but in database" "Shown in toast if user clicks on quote references messages not loaded in view, but in database"
}, },
"voiceNoteMustBeOnlyAttachment": {
"message":
"A voice note must be the only attachment included in a message.",
"description":
"Shown in toast if tries to record a voice note with any staged attachments"
},
"you": { "you": {
"message": "You", "message": "You",
"description": "description":
@ -910,6 +932,11 @@
"description": "description":
"Used for the icon layered on top of an image in message bubbles" "Used for the icon layered on top of an image in message bubbles"
}, },
"addACaption": {
"message": "Add a caption...",
"descripton":
"Used as the placeholder text in the caption editor text field"
},
"fileIconAlt": { "fileIconAlt": {
"message": "File icon", "message": "File icon",
"description": "description":

View file

@ -118,9 +118,9 @@
<div class='bottom-bar' id='footer'> <div class='bottom-bar' id='footer'>
<div class='emoji-panel-container'></div> <div class='emoji-panel-container'></div>
<div class='attachment-list'></div>
<div class='compose'> <div class='compose'>
<form class='send clearfix'> <form class='send clearfix file-input'>
<div class='attachment-previews'></div>
<div class='flex'> <div class='flex'>
<button class='emoji'></button> <button class='emoji'></button>
<textarea class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto'></textarea> <textarea class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto'></textarea>
@ -160,15 +160,6 @@
</div> </div>
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='attachment-preview'>
<div class='image-container'>
<img src='{{ source }}' class='preview' />
<div class='outer'>
<div class='play icon'></div>
</div>
</div>
<a class='x close' alt='remove attachment' href='#'></a>
</script>
<script type='text/x-tmpl-mustache' id='file-view'> <script type='text/x-tmpl-mustache' id='file-view'>
<div class='icon {{ mediaType }}'></div> <div class='icon {{ mediaType }}'></div>
<div class='text'> <div class='text'>
@ -619,7 +610,6 @@
<script type='text/javascript' src='js/views/last_seen_indicator_view.js'></script> <script type='text/javascript' src='js/views/last_seen_indicator_view.js'></script>
<script type='text/javascript' src='js/views/scroll_down_button_view.js'></script> <script type='text/javascript' src='js/views/scroll_down_button_view.js'></script>
<script type='text/javascript' src='js/views/toast_view.js'></script> <script type='text/javascript' src='js/views/toast_view.js'></script>
<script type='text/javascript' src='js/views/attachment_preview_view.js'></script>
<script type='text/javascript' src='js/views/file_input_view.js'></script> <script type='text/javascript' src='js/views/file_input_view.js'></script>
<script type='text/javascript' src='js/views/list_view.js'></script> <script type='text/javascript' src='js/views/list_view.js'></script>
<script type='text/javascript' src='js/views/conversation_list_item_view.js'></script> <script type='text/javascript' src='js/views/conversation_list_item_view.js'></script>

View file

@ -0,0 +1 @@
<svg id="Shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>add-caption-24</title><rect x="16" y="14" width="7" height="1"/><rect x="16" y="14" width="7" height="1" transform="translate(5 34) rotate(-90)"/><rect x="2" y="11" width="15" height="1"/><rect x="2" y="8" width="18" height="1"/><rect x="2" y="14" width="12" height="1"/></svg>

After

Width:  |  Height:  |  Size: 355 B

1
images/x-16.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>x-16</title><polygon points="14.35 2.35 13.65 1.65 8 7.29 2.35 1.65 1.65 2.35 7.29 8 1.65 13.65 2.35 14.35 8 8.71 13.65 14.35 14.35 13.65 8.71 8 14.35 2.35"/></svg>

After

Width:  |  Height:  |  Size: 242 B

1
images/x-shadow-16.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Shape" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 12 12"><defs><style>.cls-1{opacity:0.5;filter:url(#shadow_blur_2);}.cls-2{fill:#fff;}</style><filter id="shadow_blur_2" name="shadow_blur_2"><feGaussianBlur stdDeviation="0.5" in="SourceGraphic"/></filter></defs><title>x-shadow-12</title><g class="cls-1"><polygon points="10.6 2.6 9.9 1.9 6 5.79 2.1 1.9 1.4 2.6 5.29 6.5 1.4 10.4 2.1 11.1 6 7.21 9.9 11.1 10.6 10.4 6.71 6.5 10.6 2.6"/></g><polygon class="cls-2" points="10.6 2.1 9.9 1.4 6 5.29 2.1 1.4 1.4 2.1 5.29 6 1.4 9.9 2.1 10.6 6 6.71 9.9 10.6 10.6 9.9 6.71 6 10.6 2.1"/></svg>

After

Width:  |  Height:  |  Size: 641 B

View file

@ -15,6 +15,10 @@ const Metadata = require('./metadata/SecretSessionCipher');
const RefreshSenderCertificate = require('./refresh_sender_certificate'); const RefreshSenderCertificate = require('./refresh_sender_certificate');
// Components // Components
const {
AttachmentList,
} = require('../../ts/components/conversation/AttachmentList');
const { CaptionEditor } = require('../../ts/components/CaptionEditor');
const { const {
ContactDetail, ContactDetail,
} = require('../../ts/components/conversation/ContactDetail'); } = require('../../ts/components/conversation/ContactDetail');
@ -172,6 +176,8 @@ exports.setup = (options = {}) => {
}); });
const Components = { const Components = {
AttachmentList,
CaptionEditor,
ContactDetail, ContactDetail,
ContactListItem, ContactListItem,
ContactName, ContactName,

View file

@ -1,16 +0,0 @@
/* global Whisper */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.AttachmentPreviewView = Whisper.View.extend({
className: 'attachment-preview',
templateName: 'attachment-preview',
render_attributes() {
return { source: this.src };
},
});
})();

View file

@ -57,6 +57,11 @@
return { toastMessage: i18n('messageFoundButNotLoaded') }; return { toastMessage: i18n('messageFoundButNotLoaded') };
}, },
}); });
Whisper.VoiceNoteMustBeOnlyAttachmentToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('voiceNoteMustBeOnlyAttachment') };
},
});
Whisper.ConversationLoadingScreen = Whisper.View.extend({ Whisper.ConversationLoadingScreen = Whisper.View.extend({
templateName: 'conversation-loading-screen', templateName: 'conversation-loading-screen',
@ -144,8 +149,7 @@
this.window = options.window; this.window = options.window;
this.fileInput = new Whisper.FileInputView({ this.fileInput = new Whisper.FileInputView({
el: this.$('form.send'), el: this.$('.attachment-list'),
window: this.window,
}); });
const getHeaderProps = () => { const getHeaderProps = () => {
@ -255,15 +259,41 @@
'farFromBottom .message-list': 'addScrollDownButton', 'farFromBottom .message-list': 'addScrollDownButton',
'lazyScroll .message-list': 'onLazyScroll', 'lazyScroll .message-list': 'onLazyScroll',
'force-resize': 'forceUpdateMessageFieldSize', 'force-resize': 'forceUpdateMessageFieldSize',
dragover: 'sendToFileInput',
drop: 'sendToFileInput', 'click button.paperclip': 'onChooseAttachment',
dragleave: 'sendToFileInput', 'change input.file-input': 'onChoseAttachment',
dragover: 'onDragOver',
dragleave: 'onDragLeave',
drop: 'onDrop',
paste: 'onPaste',
}, },
sendToFileInput(e) {
if (e.originalEvent.dataTransfer.types[0] !== 'Files') { onChooseAttachment(e) {
return; e.stopPropagation();
} e.preventDefault();
this.fileInput.$el.trigger(e);
this.$('input.file-input').click();
},
async onChoseAttachment() {
const fileField = this.$('input.file-input');
const file = fileField.prop('files')[0];
await this.fileInput.maybeAddAttachment(file);
this.toggleMicrophone();
fileField.val(null);
},
onDragOver(e) {
this.fileInput.onDragOver(e);
},
onDragLeave(e) {
this.fileInput.onDragLeave(e);
},
onDrop(e) {
this.fileInput.onDrop(e);
},
onPaste(e) {
this.fileInput.onPaste(e);
}, },
onPrune() { onPrune() {
@ -483,6 +513,13 @@
captureAudio(e) { captureAudio(e) {
e.preventDefault(); e.preventDefault();
if (this.fileInput.hasFiles()) {
const toast = new Whisper.VoiceNoteMustBeOnlyAttachmentToast();
toast.$el.appendTo(this.$el);
toast.render();
return;
}
// Note - clicking anywhere will close the audio capture panel, due to // Note - clicking anywhere will close the audio capture panel, due to
// the onClick handler in InboxView, which calls its closeRecording method. // the onClick handler in InboxView, which calls its closeRecording method.
@ -503,9 +540,11 @@
this.$('.microphone').hide(); this.$('.microphone').hide();
}, },
handleAudioCapture(blob) { handleAudioCapture(blob) {
this.fileInput.file = blob; this.fileInput.addAttachment({
this.fileInput.isVoiceNote = true; contentType: blob.type,
this.fileInput.previewImages(); file: blob,
isVoiceNote: true,
});
this.$('.bottom-bar form').submit(); this.$('.bottom-bar form').submit();
}, },
endCaptureAudio() { endCaptureAudio() {
@ -1576,7 +1615,7 @@
this.setQuoteMessage(null); this.setQuoteMessage(null);
this.focusMessageFieldAndClearDisabled(); this.focusMessageFieldAndClearDisabled();
this.forceUpdateMessageFieldSize(e); this.forceUpdateMessageFieldSize(e);
this.fileInput.deleteFiles(); this.fileInput.clearAttachments();
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'Error pulling attached files before send', 'Error pulling attached files before send',

View file

@ -30,91 +30,405 @@
}, },
}); });
Whisper.UnsupportedFileTypeToast = Whisper.ToastView.extend({
template: i18n('unsupportedFileType'),
});
Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({ Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({
template: i18n('dangerousFileType'), 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({ Whisper.FileInputView = Backbone.View.extend({
tagName: 'span', tagName: 'span',
className: 'file-input', className: 'file-input',
initialize(options) { initialize() {
this.$input = this.$('input[type=file]'); this.attachments = [];
this.$input.click(e => {
e.stopPropagation(); 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: { remove() {
'change .choose-file': 'previewImages', if (this.attachmentListView) {
'click .close': 'deleteFiles', this.attachmentListView.remove();
'click .choose-file': 'open', }
drop: 'openDropped', if (this.captionEditorView) {
dragover: 'showArea', this.captionEditorView.remove();
dragleave: 'hideArea', }
paste: 'onPaste',
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(); e.preventDefault();
// hack this.$el.addClass('dropoff');
if (this.window && this.window.chrome && this.window.chrome.fileSystem) { },
this.window.chrome.fileSystem.chooseEntry(
{ type: 'openFile' }, onDragLeave(e) {
entry => { if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
if (!entry) { return;
return; }
}
entry.file(file => { e.stopPropagation();
this.file = file; e.preventDefault();
this.previewImages(); this.$el.removeClass('dropoff');
}); },
}
); onDrop(e) {
} else { if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
this.$input.click(); 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 = {}) { // Public interface
_.defaults(options, { addPlayIcon: false });
this.$('.avatar').hide();
this.thumb.src = src;
this.$('.attachment-previews').append(this.thumb.render().el);
if (options.addPlayIcon) { hasFiles() {
this.$el.addClass('video-attachment'); return this.attachments.length > 0;
} else {
this.$el.removeClass('video-attachment');
}
this.thumb.$('img')[0].onload = () => {
this.$el.trigger('force-resize');
};
this.thumb.$('img')[0].onerror = () => {
this.unableToLoadAttachment();
};
}, },
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(); const toast = new Whisper.UnableToLoadToast();
toast.$el.insertAfter(this.$el); toast.$el.insertAfter(this.$el);
toast.render(); toast.render();
this.deleteFiles();
}, },
autoScale(file) { showDangerousError() {
if (file.type.split('/')[0] !== 'image' || file.type === 'image/tiff') { 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 // nothing to do
return Promise.resolve(file); return Promise.resolve(attachment);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -132,13 +446,13 @@
img.naturalHeight <= maxHeight && img.naturalHeight <= maxHeight &&
file.size <= maxSize file.size <= maxSize
) { ) {
resolve(file); resolve(attachment);
return; return;
} }
const gifMaxSize = 25000 * 1024; const gifMaxSize = 25000 * 1024;
if (file.type === 'image/gif' && file.size <= gifMaxSize) { if (file.type === 'image/gif' && file.size <= gifMaxSize) {
resolve(file); resolve(attachment);
return; return;
} }
@ -170,285 +484,47 @@
} }
} while (i > 0 && blob.size > maxSize); } while (i > 0 && blob.size > maxSize);
resolve(blob); resolve({
...attachment,
file: blob,
});
}; };
img.src = url; img.src = url;
}); });
}, },
async previewImages() { async getFile(attachment) {
this.clearForm(); if (!attachment) {
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) {
return Promise.resolve(); return Promise.resolve();
} }
const attachmentFlags = this.isVoiceNote const attachmentFlags = attachment.isVoiceNote
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE ? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
: null; : null;
const setFlags = flags => attachment => { const scaled = await this.autoScale(attachment);
const newAttachment = Object.assign({}, attachment); const fileRead = await this.readFile(scaled);
if (flags) { return {
newAttachment.flags = flags; ...fileRead,
} url: undefined,
return newAttachment; 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() { readFile(attachment) {
// 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) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const FR = new FileReader(); const FR = new FileReader();
FR.onload = e => { FR.onload = e => {
resolve({ resolve({
...attachment,
data: e.target.result, data: e.target.result,
contentType: file.type,
fileName: file.name,
size: file.size,
}); });
}; };
FR.onerror = reject; FR.onerror = reject;
FR.onabort = 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();
}
},
}); });
})(); })();

View file

@ -30,7 +30,7 @@ function OutgoingMessage(
this.failoverNumbers = []; this.failoverNumbers = [];
this.unidentifiedDeliveries = []; this.unidentifiedDeliveries = [];
const { numberInfo, senderCertificate, online } = options; const { numberInfo, senderCertificate, online } = options || {};
this.numberInfo = numberInfo; this.numberInfo = numberInfo;
this.senderCertificate = senderCertificate; this.senderCertificate = senderCertificate;
this.online = online; this.online = online;

View file

@ -2033,6 +2033,7 @@
position: relative; position: relative;
display: inline-block; display: inline-block;
margin: 1px; margin: 1px;
vertical-align: middle;
} }
.module-image__caption-icon { .module-image__caption-icon {
@ -2041,6 +2042,14 @@
left: 6px; left: 6px;
} }
.module-image__with-click-handler {
cursor: pointer;
}
.module-image--soft-corners {
border-radius: 4px;
}
.module-image--curved-top-left { .module-image--curved-top-left {
border-top-left-radius: 16px; border-top-left-radius: 16px;
} }
@ -2143,6 +2152,17 @@
text-align: center; text-align: center;
} }
.module-image__close-button {
cursor: pointer;
position: absolute;
top: 5px;
right: 5px;
width: 16px;
height: 16px;
z-index: 2;
background-image: url('../images/x-shadow-16.svg');
}
// Module: Image Grid // Module: Image Grid
.module-image-grid { .module-image-grid {
@ -2256,6 +2276,220 @@
flex-grow: 1; flex-grow: 1;
} }
// Module: Attachments
.module-attachments {
border-top: 1px solid $color-black-015;
}
.module-attachments__header {
height: 24px;
position: relative;
}
.module-attachments__close-button {
cursor: pointer;
position: absolute;
top: 8px;
right: 16px;
width: 20px;
height: 20px;
z-index: 2;
@include color-svg('../images/x-16.svg', $color-black);
}
.module-attachments__rail {
margin-top: 12px;
margin-left: 16px;
padding-right: 16px;
overflow-x: scroll;
max-height: 142px;
white-space: nowrap;
overflow-y: hidden;
margin-bottom: 6px;
}
// Module: Staged Generic Attachment
.module-staged-generic-attachment {
height: 120px;
width: 120px;
margin: 1px;
display: inline-block;
position: relative;
border-radius: 4px;
box-shadow: inset 0px 0px 0px 1px $color-black-015;
background-color: $color-gray-05;
vertical-align: middle;
}
.module-staged-generic-attachment__close-button {
cursor: pointer;
position: absolute;
top: 5px;
right: 5px;
width: 16px;
height: 16px;
z-index: 2;
@include color-svg('../images/x-16.svg', $color-black);
}
.module-staged-generic-attachment__icon {
margin-top: 30px;
background: url('../images/file-gradient.svg') no-repeat center;
height: 44px;
width: 56px;
margin-left: 32px;
margin-right: 32px;
margin-bottom: -4px;
// So we can center the extension text inside this icon
display: flex;
flex-direction: row;
align-items: center;
}
.module-staged-generic-attachment__icon__extension {
font-size: 10px;
line-height: 13px;
letter-spacing: 0.1px;
text-transform: uppercase;
// Along with flow layout in parent item, centers text
text-align: center;
width: 25px;
margin-left: auto;
margin-right: auto;
// We don't have much room for text here, cut it off without ellipse
overflow-x: hidden;
white-space: nowrap;
text-overflow: clip;
color: $color-gray-90;
}
.module-staged-generic-attachment__filename {
margin: 7px;
margin-top: 5px;
text-align: center;
font-family: Roboto;
font-size: 14px;
overflow: hidden;
height: 2.4em;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
}
// Module: Caption Editor
.module-caption-editor {
background-color: $color-black;
z-index: 20;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
height: 100%;
}
.module-caption-editor__close-button {
z-index: 21;
cursor: pointer;
position: absolute;
top: 12px;
right: 16px;
width: 24px;
height: 24px;
z-index: 2;
@include color-svg('../images/x-16.svg', $color-white);
}
.module-caption-editor__media-container {
flex-grow: 1;
flex-shrink: 1;
background-color: $color-black;
text-align: center;
margin: 50px;
overflow: hidden;
height: 100%;
}
.module-caption-editor__image {
width: 100%;
height: 100%;
object-fit: contain;
flex-grow: 1;
flex-shrink: 1;
}
.module-caption-editor__video {
max-width: 100%;
max-height: 100%;
object-fit: contain;
flex-grow: 1;
flex-shrink: 1;
}
.module-caption-editor__placeholder {
width: 100%;
height: 100%;
object-fit: contain;
flex-grow: 1;
flex-shrink: 1;
}
.module-caption-editor__bottom-bar {
flex-grow: 0;
flex-shrink: 0;
height: 3em;
padding: 0.5em;
display: inline-flex;
flex-direction: row;
align-items: middle;
margin-left: auto;
margin-right: auto;
}
.module-caption-editor__add-caption-button {
display: inline-block;
margin-left: 6px;
height: 24px;
width: 24px;
margin-right: 6px;
@include color-svg('../images/add-caption-24.svg', $color-white);
}
.module-caption-editor__caption-input {
height: 2em;
width: 40em;
border: 1px solid $color-white;
border-radius: 1em;
color: $color-white;
background-color: $color-black;
padding: 0.5em;
&::placeholder {
color: $color-white-07;
}
}
// Third-party module: react-contextmenu // Third-party module: react-contextmenu
.react-contextmenu { .react-contextmenu {

View file

@ -0,0 +1,72 @@
## Image
```js
let caption = null;
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<CaptionEditor
url={util.gifObjectUrl}
attachment={{
contentType: 'image/jpeg',
}}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
i18n={util.i18n}
/>
</div>;
```
## Image with caption
```js
let caption =
"This is the user-provided caption. We show it overlaid on the image. If it's really long, then it wraps, but it doesn't get too close to the edges of the image.";
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<CaptionEditor
url="https://placekitten.com/800/600"
attachment={{
contentType: 'image/jpeg',
}}
caption={caption}
contentType="image/jpeg"
onChangeCaption={caption => console.log('onChangeCaption', caption)}
i18n={util.i18n}
/>
</div>;
```
## Video
```js
let caption = null;
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<CaptionEditor
url="fixtures/pixabay-Soap-Bubble-7141.mp4"
attachment={{
contentType: 'video/mp4',
}}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
i18n={util.i18n}
/>
</div>;
```
## Video with caption
```js
let caption =
"This is the user-provided caption. We show it overlaid on the image. If it's really long, then it wraps, but it doesn't get too close to the edges of the image.";
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<CaptionEditor
url="fixtures/pixabay-Soap-Bubble-7141.mp4"
attachment={{
contentType: 'video/mp4',
}}
caption={caption}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
i18n={util.i18n}
/>
</div>;
```

View file

@ -0,0 +1,78 @@
// tslint:disable:react-a11y-anchors
import React from 'react';
import * as GoogleChrome from '../util/GoogleChrome';
import { AttachmentType } from './conversation/types';
import { Localizer } from '../types/Util';
interface Props {
attachment: AttachmentType;
i18n: Localizer;
url: string;
caption?: string;
onChangeCaption?: (caption: string) => void;
close?: () => void;
}
export class CaptionEditor extends React.Component<Props> {
public renderObject() {
const { url, i18n, attachment } = this.props;
const { contentType } = attachment || { contentType: null };
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
if (isImageTypeSupported) {
return (
<img
className="module-caption-editor__image"
alt={i18n('imageAttachmentAlt')}
src={url}
/>
);
}
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
if (isVideoTypeSupported) {
return (
<video className="module-caption-editor__video" controls={true}>
<source src={url} />
</video>
);
}
return <div className="module-caption-editor__placeholder" />;
}
public render() {
const { caption, i18n, close, onChangeCaption } = this.props;
return (
<div className="module-caption-editor">
<div
role="button"
onClick={close}
className="module-caption-editor__close-button"
/>
<div className="module-caption-editor__media-container">
{this.renderObject()}
</div>
<div className="module-caption-editor__bottom-bar">
<div className="module-caption-editor__add-caption-button" />
<input
type="text"
value={caption || ''}
maxLength={200}
placeholder={i18n('addACaption')}
className="module-caption-editor__caption-input"
onChange={event => {
if (onChangeCaption) {
onChangeCaption(event.target.value);
}
}}
/>
</div>
</div>
);
}
}

View file

@ -0,0 +1,114 @@
### One image
```jsx
const attachments = [
{
url: util.gifObjectUrl,
contentType: 'image/gif',
width: 320,
height: 240,
},
];
<AttachmentList
attachments={attachments}
onClose={() => console.log('onClose')}
onClickAttachment={attachment => {
console.log('onClickAttachment', attachment);
}}
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
i18n={util.i18n}
/>;
```
### Four images
```jsx
const attachments = [
{
url: util.gifObjectUrl,
contentType: 'image/png',
width: 320,
height: 240,
},
{
url: util.pngObjectUrl,
contentType: 'image/png',
width: 800,
height: 1200,
},
{
url: util.landscapeObjectUrl,
contentType: 'image/png',
width: 4496,
height: 3000,
},
{
url: util.landscapeGreenObjectUrl,
contentType: 'image/png',
width: 1000,
height: 50,
},
];
<div>
<AttachmentList
attachments={attachments}
onClose={() => console.log('onClose')}
onClickAttachment={attachment => {
console.log('onClickAttachment', attachment);
}}
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
i18n={util.i18n}
/>
</div>;
```
### A mix of attachment types
```jsx
const attachments = [
{
url: util.gifObjectUrl,
contentType: 'image/gif',
width: 320,
height: 240,
},
{
contentType: 'text/plain',
fileName: 'manifesto.txt',
},
{
url: util.pngObjectUrl,
contentType: 'image/png',
width: 800,
height: 1200,
},
];
<div>
<AttachmentList
attachments={attachments}
onClose={() => console.log('onClose')}
onClickAttachment={attachment => {
console.log('onClickAttachment', attachment);
}}
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
i18n={util.i18n}
/>
</div>;
```
### No attachments provided
Nothing is shown if attachment list is empty.
```jsx
<AttachmentList attachments={[]} i18n={util.i18n} />
```

View file

@ -0,0 +1,106 @@
import React from 'react';
// import classNames from 'classnames';
import {
isImageTypeSupported,
isVideoTypeSupported,
} from '../../util/GoogleChrome';
import { AttachmentType } from './types';
import { Image } from './Image';
import { StagedGenericAttachment } from './StagedGenericAttachment';
import { Localizer } from '../../types/Util';
interface Props {
attachments: Array<AttachmentType>;
i18n: Localizer;
// onError: () => void;
onClickAttachment: (attachment: AttachmentType) => void;
onCloseAttachment: (attachment: AttachmentType) => void;
onClose: () => void;
}
const IMAGE_WIDTH = 120;
const IMAGE_HEIGHT = 120;
export class AttachmentList extends React.Component<Props> {
// tslint:disable-next-line max-func-body-length */
public render() {
const {
attachments,
i18n,
// onError,
onClickAttachment,
onCloseAttachment,
onClose,
} = this.props;
if (!attachments.length) {
return null;
}
return (
<div className="module-attachments">
{attachments.length > 1 ? (
<div className="module-attachments__header">
<div
role="button"
onClick={onClose}
className="module-attachments__close-button"
/>
</div>
) : null}
<div className="module-attachments__rail">
{(attachments || []).map((attachment, index) => {
const { contentType } = attachment;
if (
isImageTypeSupported(contentType) ||
isVideoTypeSupported(contentType)
) {
return (
<Image
key={getUrl(attachment) || attachment.fileName || index}
alt={`TODO: attachment number ${index}`}
i18n={i18n}
attachment={attachment}
softCorners={true}
playIconOverlay={isVideoAttachment(attachment)}
height={IMAGE_HEIGHT}
width={IMAGE_WIDTH}
url={getUrl(attachment)}
closeButton={true}
onClick={onClickAttachment}
onClickClose={onCloseAttachment}
/>
);
}
return (
<StagedGenericAttachment
key={getUrl(attachment) || attachment.fileName || index}
attachment={attachment}
i18n={i18n}
onClose={onCloseAttachment}
/>
);
})}
</div>
</div>
);
}
}
export function isVideoAttachment(attachment?: AttachmentType) {
return (
attachment &&
attachment.contentType &&
isVideoTypeSupported(attachment.contentType)
);
}
function getUrl(attachment: AttachmentType) {
if (attachment.screenshot) {
return attachment.screenshot.url;
}
return attachment.url;
}

View file

@ -77,18 +77,21 @@
width="199" width="199"
attachment={{ caption: 'dogs playing' }} attachment={{ caption: 'dogs playing' }}
url={util.pngObjectUrl} url={util.pngObjectUrl}
i18n={util.i18n}
/> />
<Image <Image
height="149" height="149"
width="149" width="149"
attachment={{ caption: 'dogs playing' }} attachment={{ caption: 'dogs playing' }}
url={util.pngObjectUrl} url={util.pngObjectUrl}
i18n={util.i18n}
/> />
<Image <Image
height="99" height="99"
width="99" width="99"
attachment={{ caption: 'dogs playing' }} attachment={{ caption: 'dogs playing' }}
url={util.pngObjectUrl} url={util.pngObjectUrl}
i18n={util.i18n}
/> />
</div> </div>
<hr /> <hr />
@ -100,6 +103,7 @@
darkOverlay darkOverlay
overlayText="+3" overlayText="+3"
url={util.pngObjectUrl} url={util.pngObjectUrl}
i18n={util.i18n}
/> />
<Image <Image
height="149" height="149"
@ -108,6 +112,7 @@
darkOverlay darkOverlay
overlayText="+3" overlayText="+3"
url={util.pngObjectUrl} url={util.pngObjectUrl}
i18n={util.i18n}
/> />
<Image <Image
height="99" height="99"
@ -116,6 +121,82 @@
darkOverlay darkOverlay
overlayText="+3" overlayText="+3"
url={util.pngObjectUrl} url={util.pngObjectUrl}
i18n={util.i18n}
/>
</div>
</div>
```
### With top-right X and soft corners
```jsx
<div>
<div>
<Image
height="200"
width="199"
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
</div>
<br />
<div>
<Image
height="200"
width="199"
closeButton={true}
attachment={{ caption: 'dogs playing' }}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
closeButton={true}
attachment={{ caption: 'dogs playing' }}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
closeButton={true}
attachment={{ caption: 'dogs playing' }}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/> />
</div> </div>
</div> </div>

View file

@ -15,24 +15,29 @@ interface Props {
overlayText?: string; overlayText?: string;
bottomOverlay?: boolean; bottomOverlay?: boolean;
closeButton?: boolean;
curveBottomLeft?: boolean; curveBottomLeft?: boolean;
curveBottomRight?: boolean; curveBottomRight?: boolean;
curveTopLeft?: boolean; curveTopLeft?: boolean;
curveTopRight?: boolean; curveTopRight?: boolean;
darkOverlay?: boolean; darkOverlay?: boolean;
playIconOverlay?: boolean; playIconOverlay?: boolean;
softCorners?: boolean;
i18n: Localizer; i18n: Localizer;
onClick?: (attachment: AttachmentType) => void; onClick?: (attachment: AttachmentType) => void;
onClickClose?: (attachment: AttachmentType) => void;
onError?: () => void; onError?: () => void;
} }
export class Image extends React.Component<Props> { export class Image extends React.Component<Props> {
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
public render() { public render() {
const { const {
alt, alt,
attachment, attachment,
bottomOverlay, bottomOverlay,
closeButton,
curveBottomLeft, curveBottomLeft,
curveBottomRight, curveBottomRight,
curveTopLeft, curveTopLeft,
@ -41,9 +46,11 @@ export class Image extends React.Component<Props> {
height, height,
i18n, i18n,
onClick, onClick,
onClickClose,
onError, onError,
overlayText, overlayText,
playIconOverlay, playIconOverlay,
softCorners,
url, url,
width, width,
} = this.props; } = this.props;
@ -52,18 +59,20 @@ export class Image extends React.Component<Props> {
return ( return (
<div <div
role={onClick ? 'button' : undefined}
onClick={() => { onClick={() => {
if (onClick) { if (onClick) {
onClick(attachment); onClick(attachment);
} }
}} }}
role="button"
className={classNames( className={classNames(
'module-image', 'module-image',
onClick ? 'module-image__with-click-handler' : null,
curveBottomLeft ? 'module-image--curved-bottom-left' : null, curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null, curveBottomRight ? 'module-image--curved-bottom-right' : null,
curveTopLeft ? 'module-image--curved-top-left' : null, curveTopLeft ? 'module-image--curved-top-left' : null,
curveTopRight ? 'module-image--curved-top-right' : null curveTopRight ? 'module-image--curved-top-right' : null,
softCorners ? 'module-image--soft-corners' : null
)} )}
> >
<img <img
@ -88,9 +97,22 @@ export class Image extends React.Component<Props> {
curveTopRight ? 'module-image--curved-top-right' : null, curveTopRight ? 'module-image--curved-top-right' : null,
curveBottomLeft ? 'module-image--curved-bottom-left' : null, curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null, curveBottomRight ? 'module-image--curved-bottom-right' : null,
softCorners ? 'module-image--soft-corners' : null,
darkOverlay ? 'module-image__border-overlay--dark' : null darkOverlay ? 'module-image__border-overlay--dark' : null
)} )}
/> />
{closeButton ? (
<div
role="button"
onClick={(e: React.MouseEvent<{}>) => {
e.stopPropagation();
if (onClickClose) {
onClickClose(attachment);
}
}}
className="module-image__close-button"
/>
) : null}
{bottomOverlay ? ( {bottomOverlay ? (
<div <div
className={classNames( className={classNames(

View file

@ -24,7 +24,7 @@ interface Props {
const MAX_WIDTH = 300; const MAX_WIDTH = 300;
const MAX_HEIGHT = MAX_WIDTH * 1.5; const MAX_HEIGHT = MAX_WIDTH * 1.5;
const MIN_WIDTH = 200; const MIN_WIDTH = 200;
const MIN_HEIGHT = 25; const MIN_HEIGHT = 50;
export class ImageGrid extends React.Component<Props> { export class ImageGrid extends React.Component<Props> {
// tslint:disable-next-line max-func-body-length */ // tslint:disable-next-line max-func-body-length */

View file

@ -79,52 +79,6 @@ interface State {
imageBroken: boolean; imageBroken: boolean;
} }
function isAudio(attachments?: Array<AttachmentType>) {
return (
attachments &&
attachments[0] &&
attachments[0].contentType &&
MIME.isAudio(attachments[0].contentType)
);
}
function canDisplayImage(attachments?: Array<AttachmentType>) {
const { height, width } =
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
return (
height &&
height > 0 &&
height <= 4096 &&
width &&
width > 0 &&
width <= 4096
);
}
function getExtension({
fileName,
contentType,
}: {
fileName: string;
contentType: MIME.MIMEType;
}): string | null {
if (fileName && fileName.indexOf('.') >= 0) {
const lastPeriod = fileName.lastIndexOf('.');
const extension = fileName.slice(lastPeriod + 1);
if (extension.length) {
return extension;
}
}
const slash = contentType.indexOf('/');
if (slash >= 0) {
return contentType.slice(slash + 1);
}
return null;
}
const EXPIRATION_CHECK_MINIMUM = 2000; const EXPIRATION_CHECK_MINIMUM = 2000;
const EXPIRED_DELAY = 600; const EXPIRED_DELAY = 600;
@ -847,3 +801,49 @@ export class Message extends React.Component<Props, State> {
); );
} }
} }
export function getExtension({
fileName,
contentType,
}: {
fileName: string;
contentType: MIME.MIMEType;
}): string | null {
if (fileName && fileName.indexOf('.') >= 0) {
const lastPeriod = fileName.lastIndexOf('.');
const extension = fileName.slice(lastPeriod + 1);
if (extension.length) {
return extension;
}
}
const slash = contentType.indexOf('/');
if (slash >= 0) {
return contentType.slice(slash + 1);
}
return null;
}
function isAudio(attachments?: Array<AttachmentType>) {
return (
attachments &&
attachments[0] &&
attachments[0].contentType &&
MIME.isAudio(attachments[0].contentType)
);
}
function canDisplayImage(attachments?: Array<AttachmentType>) {
const { height, width } =
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
return (
height &&
height > 0 &&
height <= 4096 &&
width &&
width > 0 &&
width <= 4096
);
}

View file

@ -0,0 +1,44 @@
Text file
```js
const attachment = {
contentType: 'text/plain',
fileName: 'manifesto.txt',
};
<StagedGenericAttachment
attachment={attachment}
i18n={util.i18n}
onClose={attachment => console.log('onClose', attachment)}
/>;
```
File with long name
```js
const attachment = {
contentType: 'text/plain',
fileName: 'this-is-my-very-important-manifesto-you-must-read-it.txt',
};
<StagedGenericAttachment
attachment={attachment}
i18n={util.i18n}
onClose={attachment => console.log('onClose', attachment)}
/>;
```
File with long extension
```js
const attachment = {
contentType: 'text/plain',
fileName: 'manifesto.reallylongtxt',
};
<StagedGenericAttachment
attachment={attachment}
i18n={util.i18n}
onClose={attachment => console.log('onClose', attachment)}
/>;
```

View file

@ -0,0 +1,44 @@
import React from 'react';
import { getExtension } from './Message';
import { Localizer } from '../../types/Util';
import { AttachmentType } from './types';
interface Props {
attachment: AttachmentType;
onClose: (attachment: AttachmentType) => void;
i18n: Localizer;
}
export class StagedGenericAttachment extends React.Component<Props> {
public render() {
const { attachment, onClose } = this.props;
const { fileName, contentType } = attachment;
const extension = getExtension({ contentType, fileName });
return (
<div className="module-staged-generic-attachment">
<div
className="module-staged-generic-attachment__close-button"
role="button"
onClick={() => {
if (onClose) {
onClose(attachment);
}
}}
/>
<div className="module-staged-generic-attachment__icon">
{extension ? (
<div className="module-staged-generic-attachment__icon__extension">
{extension}
</div>
) : null}
</div>
<div className="module-staged-generic-attachment__filename">
{fileName}
</div>
</div>
);
}
}

View file

@ -659,468 +659,495 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " template: $('#conversation').html(),", "line": " template: $('#conversation').html(),",
"lineNumber": 73, "lineNumber": 78,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-html(", "rule": "jQuery-html(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " template: $('#conversation').html(),", "line": " template: $('#conversation').html(),",
"lineNumber": 73, "lineNumber": 78,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-15T00:38:04.183Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Getting the value, not setting it" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));", "line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));",
"lineNumber": 143, "lineNumber": 148,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-11-14T19:09:08.182Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-prependTo(", "rule": "jQuery-prependTo(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));", "line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));",
"lineNumber": 143, "lineNumber": 148,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-11-14T19:07:46.079Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " el: this.$('form.send'),", "line": " el: this.$('.attachment-list'),",
"lineNumber": 147, "lineNumber": 152,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-11-14T19:07:46.079Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.conversation-header').append(this.titleView.el);", "line": " this.$('.conversation-header').append(this.titleView.el);",
"lineNumber": 205, "lineNumber": 209,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.conversation-header').append(this.titleView.el);", "line": " this.$('.conversation-header').append(this.titleView.el);",
"lineNumber": 205, "lineNumber": 209,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.discussion-container').append(this.view.el);", "line": " this.$('.discussion-container').append(this.view.el);",
"lineNumber": 211, "lineNumber": 215,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.discussion-container').append(this.view.el);", "line": " this.$('.discussion-container').append(this.view.el);",
"lineNumber": 211, "lineNumber": 215,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$messageField = this.$('.send-message');", "line": " this.$messageField = this.$('.send-message');",
"lineNumber": 214, "lineNumber": 218,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.send-message').focus(this.focusBottomBar.bind(this));", "line": " this.$('.send-message').focus(this.focusBottomBar.bind(this));",
"lineNumber": 232, "lineNumber": 236,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$emojiPanelContainer = this.$('.emoji-panel-container');", "line": " this.$emojiPanelContainer = this.$('.emoji-panel-container');",
"lineNumber": 235, "lineNumber": 239,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('input.file-input').click();",
"lineNumber": 276,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const fileField = this.$('input.file-input');",
"lineNumber": 279,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " const container = this.$('.discussion-container');", "line": " const container = this.$('.discussion-container');",
"lineNumber": 421, "lineNumber": 451,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " container.append(this.banner.el);", "line": " container.append(this.banner.el);",
"lineNumber": 422, "lineNumber": 452,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));", "line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));",
"lineNumber": 459, "lineNumber": 489,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-11-14T18:51:15.180Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "$() parameter is a hard-coded string" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-appendTo(", "rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));", "line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));",
"lineNumber": 459, "lineNumber": 489,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-11-14T18:51:15.180Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Both parameters are known elements from the DOM" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.send-message').val().length > 0 ||", "line": " this.$('.send-message').val().length > 0 ||",
"lineNumber": 468, "lineNumber": 498,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.capture-audio').hide();", "line": " this.$('.capture-audio').hide();",
"lineNumber": 471, "lineNumber": 501,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.capture-audio').show();", "line": " this.$('.capture-audio').show();",
"lineNumber": 473, "lineNumber": 503,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " if (this.$('.send-message').val().length > 2000) {", "line": " if (this.$('.send-message').val().length > 2000) {",
"lineNumber": 477, "lineNumber": 507,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.android-length-warning').hide();", "line": " this.$('.android-length-warning').hide();",
"lineNumber": 480, "lineNumber": 510,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 518,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " view.$el.appendTo(this.$('.capture-audio'));", "line": " view.$el.appendTo(this.$('.capture-audio'));",
"lineNumber": 500, "lineNumber": 537,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-appendTo(", "rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " view.$el.appendTo(this.$('.capture-audio'));", "line": " view.$el.appendTo(this.$('.capture-audio'));",
"lineNumber": 500, "lineNumber": 537,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.send-message').attr('disabled', true);", "line": " this.$('.send-message').attr('disabled', true);",
"lineNumber": 502, "lineNumber": 539,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.bottom-bar form').submit();", "line": " this.$('.bottom-bar form').submit();",
"lineNumber": 509, "lineNumber": 548,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.send-message').removeAttr('disabled');", "line": " this.$('.send-message').removeAttr('disabled');",
"lineNumber": 512, "lineNumber": 551,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.bottom-bar form').removeClass('active');", "line": " this.$('.bottom-bar form').removeClass('active');",
"lineNumber": 518, "lineNumber": 557,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.bottom-bar form').addClass('active');", "line": " this.$('.bottom-bar form').addClass('active');",
"lineNumber": 521, "lineNumber": 560,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " const container = this.$('.discussion-container');", "line": " const container = this.$('.discussion-container');",
"lineNumber": 609, "lineNumber": 648,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " container.append(this.scrollDownButton.el);", "line": " container.append(this.scrollDownButton.el);",
"lineNumber": 610, "lineNumber": 649,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 637,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 670,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 674,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const el = this.$(`#${databaseId}`);",
"lineNumber": 681,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-appendTo(", "rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);", "line": " toast.$el.appendTo(this.$el);",
"lineNumber": 684, "lineNumber": 676,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 709,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 713,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const el = this.$(`#${databaseId}`);",
"lineNumber": 720,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 723,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));", "line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));",
"lineNumber": 861, "lineNumber": 900,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-insertBefore(", "rule": "jQuery-insertBefore(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));", "line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));",
"lineNumber": 861, "lineNumber": 900,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.bar-container').show();", "line": " this.$('.bar-container').show();",
"lineNumber": 916, "lineNumber": 955,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.bar-container').hide();", "line": " this.$('.bar-container').hide();",
"lineNumber": 928, "lineNumber": 967,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " const el = this.$(`#${message.id}`);", "line": " const el = this.$(`#${message.id}`);",
"lineNumber": 1025, "lineNumber": 1064,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-prepend(", "rule": "jQuery-prepend(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$el.prepend(dialog.el);", "line": " this.$el.prepend(dialog.el);",
"lineNumber": 1098, "lineNumber": 1137,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-appendTo(", "rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);", "line": " toast.$el.appendTo(this.$el);",
"lineNumber": 1121, "lineNumber": 1160,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-10-11T19:22:47.331Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Operating on already-existing DOM elements" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-prepend(", "rule": "jQuery-prepend(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$el.prepend(dialog.el);", "line": " this.$el.prepend(dialog.el);",
"lineNumber": 1149, "lineNumber": 1188,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " view.$el.insertBefore(this.$('.panel').first());", "line": " view.$el.insertBefore(this.$('.panel').first());",
"lineNumber": 1283, "lineNumber": 1323,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-insertBefore(", "rule": "jQuery-insertBefore(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " view.$el.insertBefore(this.$('.panel').first());", "line": " view.$el.insertBefore(this.$('.panel').first());",
"lineNumber": 1283, "lineNumber": 1323,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-prepend(", "rule": "jQuery-prepend(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$el.prepend(dialog.el);", "line": " this.$el.prepend(dialog.el);",
"lineNumber": 1361, "lineNumber": 1401,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.send').prepend(this.quoteView.el);", "line": " this.$('.send').prepend(this.quoteView.el);",
"lineNumber": 1531, "lineNumber": 1571,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-prepend(", "rule": "jQuery-prepend(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.send').prepend(this.quoteView.el);", "line": " this.$('.send').prepend(this.quoteView.el);",
"lineNumber": 1531, "lineNumber": 1571,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-appendTo(", "rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);", "line": " toast.$el.appendTo(this.$el);",
"lineNumber": 1555, "lineNumber": 1595,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.bottom-bar form').submit();", "line": " this.$('.bottom-bar form').submit();",
"lineNumber": 1610, "lineNumber": 1650,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " const $attachmentPreviews = this.$('.attachment-previews');", "line": " const $attachmentPreviews = this.$('.attachment-previews');",
"lineNumber": 1619, "lineNumber": 1659,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/conversation_view.js", "path": "js/views/conversation_view.js",
"line": " this.$('.panel').css('display') === 'none'", "line": " this.$('.panel').css('display') === 'none'",
"lineNumber": 1650, "lineNumber": 1690,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
@ -1196,103 +1223,58 @@
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js", "path": "js/views/file_input_view.js",
"line": " this.$input = this.$('input[type=file]');", "line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 45, "lineNumber": 216,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.$('.avatar').hide();",
"lineNumber": 88,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.$('.attachment-previews').append(this.thumb.render().el);",
"lineNumber": 90,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-append(",
"path": "js/views/file_input_view.js",
"line": " this.$('.attachment-previews').append(this.thumb.render().el);",
"lineNumber": 90,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.thumb.$('img')[0].onload = () => {",
"lineNumber": 98,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.thumb.$('img')[0].onerror = () => {",
"lineNumber": 101,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-insertAfter(", "rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js", "path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);", "line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 108, "lineNumber": 222,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 190,
"reasonCategory": "usageTrusted",
"updated": "2018-10-11T19:22:47.331Z",
"reasonDetail": "Operating on already-existing DOM elements"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 284,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.$('.avatar').show();",
"lineNumber": 388,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js", "path": "js/views/file_input_view.js",
"line": " .wrap('<form>')", "line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 398, "lineNumber": 230,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Hard-coded value" "reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 236,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 242,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 248,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
@ -3493,7 +3475,7 @@
"lineNumber": 4136, "lineNumber": 4136,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z", "updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "<optional>" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
@ -4083,7 +4065,7 @@
"lineNumber": 483, "lineNumber": 483,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "<optional>" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "jQuery-appendTo(", "rule": "jQuery-appendTo(",
@ -5849,7 +5831,7 @@
"lineNumber": 1699, "lineNumber": 1699,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-18T19:19:27.699Z", "updated": "2018-09-18T19:19:27.699Z",
"reasonDetail": "<optional>" "reasonDetail": "Protected from arbitrary input"
}, },
{ {
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",