@ -1,36 +1,42 @@
/* eslint-disable */
/* global textsecure: false */
/* global Whisper: false */
/* global i18n: false */
/* global loadImage: false */
/* global Backbone: false */
/* global _: false */
/* global Signal: false */
// eslint-disable-next-line func-names
( function ( ) {
'use strict' ;
window . Whisper = window . Whisper || { } ;
const { MIME } = window . Signal . Types ;
Whisper . FileSizeToast = Whisper . ToastView . extend ( {
templateName : 'file-size-modal' ,
render _attributes : function ( ) {
render _attributes ( ) {
return {
'file-size-warning' : i18n ( 'fileSizeWarning' ) ,
limit : this . model . limit ,
units : this . model . units
units : this . model . units ,
} ;
}
} ,
} ) ;
Whisper . UnsupportedFileTypeToast = Whisper . ToastView . extend ( {
template : i18n ( 'unsupportedFileType' )
template : i18n ( 'unsupportedFileType' ) ,
} ) ;
function mak eThumbnail( size , objectUrl ) {
return new Promise ( function ( resolve , reject ) {
var img = document . createElement ( 'img' ) ;
function mak eImag eThumbnail( size , objectUrl ) {
return new Promise ( ( ( resolve , reject ) => {
const img = document . createElement ( 'img' ) ;
img . onerror = reject ;
img . onload = function ( ) {
img . onload = ( ) => {
// using components/blueimp-load-image
// first, make the correct size
var canvas = loadImage . scale ( img , {
let canvas = loadImage . scale ( img , {
canvas : true ,
cover : true ,
maxWidth : size ,
@ -49,20 +55,75 @@
minHeight : size ,
} ) ;
var blob = window . dataURLToBlobSync ( canvas . toDataURL ( 'image/png' ) ) ;
const blob = window . dataURLToBlobSync ( canvas . toDataURL ( 'image/png' ) ) ;
resolve ( blob ) ;
} ;
img . src = objectUrl ;
} ) ) ;
}
function makeVideoScreenshot ( objectUrl ) {
return new Promise ( ( ( resolve , reject ) => {
const video = document . createElement ( 'video' ) ;
function capture ( ) {
const canvas = document . createElement ( 'canvas' ) ;
canvas . width = video . videoWidth ;
canvas . height = video . videoHeight ;
canvas . getContext ( '2d' ) . drawImage ( video , 0 , 0 , canvas . width , canvas . height ) ;
const image = window . dataURLToBlobSync ( canvas . toDataURL ( 'image/png' ) ) ;
video . removeEventListener ( 'canplay' , capture ) ;
resolve ( image ) ;
}
video . addEventListener ( 'canplay' , capture ) ;
video . addEventListener ( 'error' , ( error ) => {
console . log (
'makeVideoThumbnail error' ,
Signal . Types . Errors . toLogFormat ( error )
) ;
reject ( error ) ;
} ) ;
video . src = objectUrl ;
} ) ) ;
}
function blobToArrayBuffer ( blob ) {
return new Promise ( ( resolve , reject ) => {
const fileReader = new FileReader ( ) ;
fileReader . onload = e => resolve ( e . target . result ) ;
fileReader . onerror = reject ;
fileReader . onabort = reject ;
fileReader . readAsArrayBuffer ( blob ) ;
} ) ;
}
async function makeVideoThumbnail ( size , videoObjectUrl ) {
const blob = await makeVideoScreenshot ( videoObjectUrl ) ;
const data = await blobToArrayBuffer ( blob ) ;
const screenshotObjectUrl = Signal . Util . arrayBufferToObjectURL ( {
data ,
type : 'image/png' ,
} ) ;
const thumbnail = await makeImageThumbnail ( size , screenshotObjectUrl ) ;
URL . revokeObjectURL ( screenshotObjectUrl ) ;
return thumbnail ;
}
Whisper . FileInputView = Backbone . View . extend ( {
tagName : 'span' ,
className : 'file-input' ,
initialize : function ( options ) {
initialize ( options ) {
this . $input = this . $ ( 'input[type=file]' ) ;
this . $input . click ( function ( e ) {
this . $input . click ( ( e ) => {
e . stopPropagation ( ) ;
} ) ;
this . thumb = new Whisper . AttachmentPreviewView ( ) ;
@ -75,76 +136,81 @@
'change .choose-file' : 'previewImages' ,
'click .close' : 'deleteFiles' ,
'click .choose-file' : 'open' ,
'drop' : 'openDropped' ,
'dragover' : 'showArea' ,
'dragleave' : 'hideArea' ,
'paste' : 'onPaste'
drop : 'openDropped' ,
dragover : 'showArea' ,
dragleave : 'hideArea' ,
paste : 'onPaste' ,
} ,
ope n: functio n( e ) {
ope n( e ) {
e . preventDefault ( ) ;
// hack
if ( this . window && this . window . chrome && this . window . chrome . fileSystem ) {
this . window . chrome . fileSystem . chooseEntry ( { type : 'openFile' } , function ( entry ) {
this . window . chrome . fileSystem . chooseEntry ( { type : 'openFile' } , ( entry ) => {
if ( ! entry ) {
return ;
}
entry . file ( function ( file ) {
entry . file ( ( file ) => {
this . file = file ;
this . previewImages ( ) ;
} . bind ( this ) ) ;
} . bind ( this ) ) ;
} ) ;
} ) ;
} else {
this . $input . click ( ) ;
}
} ,
addThumb : function ( src ) {
addThumb ( src , options = { } ) {
_ . defaults ( options , { addPlayIcon : false } ) ;
this . $ ( '.avatar' ) . hide ( ) ;
this . thumb . src = src ;
this . $ ( '.attachment-previews' ) . append ( this . thumb . render ( ) . el ) ;
this . thumb . $ ( 'img' ) [ 0 ] . onload = function ( ) {
if ( options . addPlayIcon ) {
this . $el . addClass ( 'video-attachment' ) ;
} else {
this . $el . removeClass ( 'video-attachment' ) ;
}
this . thumb . $ ( 'img' ) [ 0 ] . onload = ( ) => {
this . $el . trigger ( 'force-resize' ) ;
} . bind ( this ) ;
} ;
} ,
autoScale : function ( file ) {
if ( file . type . split ( '/' ) [ 0 ] !== 'image'
|| file . type === 'image/gif'
|| file . type === 'image/tiff' ) {
autoScale ( file ) {
if ( file . type . split ( '/' ) [ 0 ] !== 'image' ||
file . type === 'image/gif' ||
file . type === 'image/tiff' ) {
// nothing to do
return Promise . resolve ( file ) ;
}
return new Promise ( function ( resolve , reject ) {
var url = URL . createObjectURL ( file ) ;
var img = document . createElement ( 'img' ) ;
return new Promise ( ( ( resolve , reject ) => {
const url = URL . createObjectURL ( file ) ;
const img = document . createElement ( 'img' ) ;
img . onerror = reject ;
img . onload = function ( ) {
img . onload = ( ) => {
URL . revokeObjectURL ( url ) ;
var maxSize = 6000 * 1024 ;
var maxHeight = 4096 ;
var maxWidth = 4096 ;
if ( img . width <= maxWidth && img . height <= maxHeight &&
file . size <= maxSize ) {
const maxSize = 6000 * 1024 ;
const maxHeight = 4096 ;
const maxWidth = 4096 ;
if ( img . width <= maxWidth && img . height <= maxHeight && file . size <= maxSize ) {
resolve ( file ) ;
return ;
}
var canvas = loadImage . scale ( img , {
canvas : true , maxWidth : maxWidth , maxHeight : maxHeight
const canvas = loadImage . scale ( img , {
canvas : true , maxWidth , maxHeight ,
} ) ;
var quality = 0.95 ;
var i = 4 ;
var blob ;
let quality = 0.95 ;
let i = 4 ;
let blob ;
do {
i = i - 1 ;
blob = window . dataURLToBlobSync (
canvas . toDataURL ( 'image/jpeg' , quality )
) ;
quality = quality * maxSize / blob . size ;
i -= 1 ;
blob = window . dataURLToBlobSync ( canvas . toDataURL ( 'image/jpeg' , quality ) ) ;
quality = ( quality * maxSize ) / blob . size ;
// NOTE: During testing with a large image, we observed the
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
@ -156,44 +222,59 @@
resolve ( blob ) ;
} ;
img . src = url ;
} ) ;
} ) ) ;
} ,
previewImages : function ( ) {
async previewImages ( ) {
this . clearForm ( ) ;
var file = this . file || this . $input . prop ( 'files' ) [ 0 ] ;
if ( ! file ) { return ; }
var type = file . type . split ( '/' ) [ 0 ] ;
if ( file . type === 'image/tiff' ) {
type = 'file' ;
const file = this . file || this . $input . prop ( 'files' ) [ 0 ] ;
if ( ! file ) {
return ;
}
switch ( type ) {
case 'audio' : this . addThumb ( 'images/audio.svg' ) ; break ;
case 'video' : this . addThumb ( 'images/video.svg' ) ; break ;
case 'image' :
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 thumbnail = await makeVideoScreenshot ( this . previewObjectUrl ) ;
URL . revokeObjectURL ( this . previewObjectUrl ) ;
const data = await blobToArrayBuffer ( thumbnail ) ;
this . previewObjectUrl = Signal . Util . arrayBufferToObjectURL ( {
data ,
type : 'image/png' ,
} ) ;
this . addThumb ( this . previewObjectUrl , { addPlayIcon : true } ) ;
} ;
const renderImagePreview = async ( ) => {
if ( ! MIME . isJPEG ( file . type ) ) {
this . previewObjectUrl = URL . createObjectURL ( file ) ;
this . addThumb ( this . previewObjectUrl ) ;
break ;
return ;
}
// NOTE: Temporarily allow `then` until we convert the entire file
// to `async` / `await`:
// eslint-disable-next-line more/no-then
window . autoOrientImage ( file )
. then ( dataURL => this . addThumb ( dataURL ) ) ;
break ;
default :
this . addThumb ( 'images/file.svg' ) ; break ;
const dataUrl = await window . autoOrientImage ( file ) ;
this . addThumb ( dataUrl ) ;
} ;
if ( Signal . Util . GoogleChrome . isImageTypeSupported ( contentType ) ) {
renderImagePreview ( ) ;
} else if ( Signal . Util . GoogleChrome . isVideoTypeSupported ( contentType ) ) {
renderVideoPreview ( ) ;
} else if ( MIME . isAudio ( contentType ) ) {
this . addThumb ( 'images/audio.svg' ) ;
} else {
this . addThumb ( 'images/file.svg' ) ;
}
// NOTE: Temporarily allow `then` until we convert the entire file
// to `async` / `await`:
// eslint-disable-next-line more/no-then
this . autoScale ( file ) . then ( function ( blob ) {
var limitKb = 1000000 ;
var blobType = file . type === 'image/gif' ? 'gif' : type ;
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 ;
@ -206,31 +287,28 @@
default :
limitKb = 100000 ; break ;
}
if ( ( blob . size / 1024 ) . toFixed ( 4 ) >= limitKb ) {
var units = [ 'kB' , 'MB' , 'GB' ] ;
var u = - 1 ;
var limit = limitKb * 1000 ;
if ( ( blob . size / 1024 ) . toFixed ( 4 ) >= limitKb ) {
const units = [ 'kB' , 'MB' , 'GB' ] ;
let u = - 1 ;
let limit = limitKb * 1000 ;
do {
limit /= 1000 ;
++ u ;
u += 1 ;
} while ( limit >= 1000 && u < units . length - 1 ) ;
var toast = new Whisper . FileSizeToast ( {
model : { limit : limit , units : units [ u ] }
const toast = new Whisper . FileSizeToast ( {
model : { limit , units : units [ u ] } ,
} ) ;
toast . $el . insertAfter ( this . $el ) ;
toast . render ( ) ;
this . deleteFiles ( ) ;
}
} . bind ( this ) ) ;
} ,
hasFiles : function ( ) {
var files = this . file ? [ this . file ] : this . $input . prop ( 'files' ) ;
hasFiles ( ) {
const files = this . file ? [ this . file ] : this . $input . prop ( 'files' ) ;
return files && files . length && files . length > 0 ;
} ,
/* eslint-enable */
/* jshint ignore:start */
getFiles ( ) {
const files = this . file ? [ this . file ] : Array . from ( this . $input . prop ( 'files' ) ) ;
const promise = Promise . all ( files . map ( file => this . getFile ( file ) ) ) ;
@ -262,44 +340,45 @@
. then ( this . readFile )
. then ( setFlags ( attachmentFlags ) ) ;
} ,
/* jshint ignore:end */
/* eslint-disable */
getThumbnail : function ( ) {
async getThumbnail ( ) {
// Scale and crop an image to 256px square
var size = 256 ;
var file = this . file || this . $input . prop ( 'files' ) [ 0 ] ;
if ( file === undefined || file . type . split ( '/' ) [ 0 ] !== 'image' || file . type === 'image/gif' ) {
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 ) ;
return makeThumbnail ( 256 , file ) . then ( function ( arrayBuffer ) {
URL . revokeObjectURL ( url ) ;
const arrayBuffer = await makeImageThumbnail ( size , objectUrl ) ;
URL . revokeObjectURL ( objectUrl ) ;
return this . readFile ( arrayBuffer ) ;
} ) ;
} ,
// File -> Promise Attachment
readFile : function ( file ) {
return new Promise ( function ( resolve , reject ) {
var FR = new FileReader ( ) ;
FR . onload = function ( e ) {
readFile ( file ) {
return new Promise ( ( ( resolve , reject ) => {
const FR = new FileReader ( ) ;
FR . onload = ( e ) => {
resolve ( {
data : e . target . result ,
contentType : file . type ,
fileName : file . name ,
size : file . size
size : file . size ,
} ) ;
} ;
FR . onerror = reject ;
FR . onabort = reject ;
FR . readAsArrayBuffer ( file ) ;
} ) ;
} ) ) ;
} ,
clearForm : function ( ) {
clearForm ( ) {
if ( this . previewObjectUrl ) {
URL . revokeObjectURL ( this . previewObjectUrl ) ;
this . previewObjectUrl = null ;
@ -310,7 +389,7 @@
this . $el . trigger ( 'force-resize' ) ;
} ,
deleteFiles : function ( e ) {
deleteFiles ( e ) {
if ( e ) { e . stopPropagation ( ) ; }
this . clearForm ( ) ;
this . $input . wrap ( '<form>' ) . parent ( 'form' ) . trigger ( 'reset' ) ;
@ -320,20 +399,22 @@
this . isVoiceNote = false ;
} ,
openDropped : function ( e ) {
if ( e . originalEvent . dataTransfer . types [ 0 ] ! = 'Files' ) {
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 : function ( e ) {
if ( e . originalEvent . dataTransfer . types [ 0 ] ! = 'Files' ) {
showArea ( e ) {
if ( e . originalEvent . dataTransfer . types [ 0 ] ! = = 'Files' ) {
return ;
}
@ -342,8 +423,8 @@
this . $el . addClass ( 'dropoff' ) ;
} ,
hideArea : function ( e ) {
if ( e . originalEvent . dataTransfer . types [ 0 ] ! = 'Files' ) {
hideArea ( e ) {
if ( e . originalEvent . dataTransfer . types [ 0 ] ! = = 'Files' ) {
return ;
}
@ -351,10 +432,10 @@
e . preventDefault ( ) ;
this . $el . removeClass ( 'dropoff' ) ;
} ,
onPaste : function ( e ) {
var items = e . originalEvent . clipboardData . items ;
var imgBlob = null ;
for ( var i = 0 ; i < items . length ; i ++ ) {
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 ( ) ;
}
@ -363,8 +444,10 @@
this . file = imgBlob ;
this . previewImages ( ) ;
}
}
} ,
} ) ;
Whisper . FileInputView . makeThumbnail = makeThumbnail ;
} ) ( ) ;
Whisper . FileInputView . makeImageThumbnail = makeImageThumbnail ;
Whisper . FileInputView . makeVideoThumbnail = makeVideoThumbnail ;
Whisper . FileInputView . makeVideoScreenshot = makeVideoScreenshot ;
} ( ) ) ;