Auto-orient image attachments based on EXIF metadata
As described in #998, images are sometimes displayed with an incorrect orientation. This is because cameras often write files in the native sensor byte order and attach the `Orientation` EXIF metadata to tell end-user devices how to display the images based on the original author’s capture orientation. Electron/Chromium (and therefore Signal Desktop) currently doesn’t support applying this metadata for `<img>` tags, e.g. CSS `image-orientation: from- image`. As a workaround, this change uses the `loadImage` library with the `orientation: true` flag to auto-orient images ~~before display~~ upon receipt and before sending. **Changes** - [x] ~~Auto-orient images during display in message list view~~ - [x] Ensure image is not displayed until loaded (to prevent layout reflow) . - [x] Auto-orient images upon receipt and before storing in IndexedDB (~~or preserve original data until Chromium offers native fix?~~) - [x] Auto-orient images in compose area preview. - [x] ~~Auto-orient images in lightbox view~~ - [x] Auto-orient images before sending / storage. - [x] Add EditorConfig for sharing code styles across editors. - [x] Fix ESLint ignore file. - [x] Change `function-paren-newline` ESLint rule from `consistent` to `multiline`. - [x] Add `operator-linebreak` ESLint rule for consistency. - [x] Added `blob-util` dependency for converting between array buffers, blobs, etc. - [x] Extracted `createMessageHandler` to consolidate logic for `onMessageReceived` and `onSentMessage`. - [x] Introduce `async` / `await` to simplify async coding (restore control flow for branching, loops, and exceptions). - [x] Introduce `window.Signal` namespace for exposing ES2015+ CommonJS modules. - [x] Introduce rudimentary `Message` and `Attachment` types to begin defining a schema and versioning. This will allow us to track which changes, e.g. auto-orient JPEGs, per message / attachment as well as which fields are stored. - [x] Renamed `window.dataURLtoBlob` to `window.dataURLToBlobSync` to both fix the strange `camelCase` as well as to highlight that this operation is synchronous and therefore blocks the user thread. - [x] Normalize all JPEG MIME types to `image/jpeg`, eliminating the invalid `image/jpg`. - [x] Add `npm run test-modules` command for testing non-browser specific CommonJS modules. - **Stretch Goals** - [ ] ~~Restrict `autoOrientImage` to `Blob` to narrow API interface.~~ Do this once we use PureScript. - [ ] ~~Return non-JPEGs as no-op from `autoOrientImage`.~~ Skipping `autoOrientImage` for non-JPEGs altogether. - [ ] Retroactively auto-orient existing JPEG image attachments in the background. --- Fixes #998 --- - **Blog:** EXIF Orientation Handling Is a Ghetto: https://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ - **Chromium Bug:** EXIF orientation is ignored: https://bugs.chromium.org/p/chromium/issues/detail?id=56845 - **Chromium Bug:** Support for the CSS image-orientation CSS property: https://bugs.chromium.org/p/chromium/issues/detail?id=158753 --- commitce5090b473
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 10:35:36 2018 -0500 Inline message descriptors commit329036e59c
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:34:40 2018 -0500 Clarify order of operations Semantically, it makes more sense to do `getFile` before `clearForm` even though it seems to work either way. commitf9d4cfb2ba
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:18:26 2018 -0500 Simplify `operator-linebreak` configuration Enabling `before` caused more code changes and it turns out our previous configuration is already the default. commitdb588997ac
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:15:59 2018 -0500 Remove obsolete TODO commit799c881763
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:12:18 2018 -0500 Enable ESLint `function-paren-newline` `multiline` Per discussion. commitb660b6bc8e
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:10:48 2018 -0500 Use `messageDescriptor.id` not `source` commit5e7309d176
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 16:29:01 2018 -0500 Remove unnecessary `eslint-env` commit393b3da55e
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 16:19:17 2018 -0500 Refactor `onSentMessage` and `onMessageReceived` Since they are so similar, we create the handlers using new `createMessageHandler` function. This allows us to ensure both synced and received messages go through schema upgrade pipeline. commitb3db0bf179
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 16:18:21 2018 -0500 Add `Message` descriptor functions commit8febf125b1
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 14:46:56 2018 -0500 Fix typo commit98d951ef77
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:22:39 2018 -0500 Remove `promises` reference commita0e9559ed5
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:22:13 2018 -0500 Fix `AttachmentView::mediaType` fall-through commit67be916a83
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:03:41 2018 -0500 Remove minor TODOs commit0af186e118
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:44:41 2018 -0500 Enable ESLint for `js/views/attachment_view.js` commit28a2dc5b8a
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:44:12 2018 -0500 Remove dynamic type checks commitf4ce36fcfc
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:27:56 2018 -0500 Rename `process` to `upgradeSchema` - `Message.process` -> `Message.upgradeSchema` - `Attachment.process` -> `Attachment.upgradeSchema` - `Attachment::processVersion` -> `Attachment::schemaVersion` Document version history. commit41b92c0a31
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:11:50 2018 -0500 Add `operator-linebreak` ESLint rule Based on the following discussion: https://github.com/signalapp/Signal-Desktop/pull/2040#discussion_r168029106 commit462defbe55
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:01:30 2018 -0500 Add missing `await` for `ConversationController.getOrCreateAndWait` Tested this by setting `if` condition to `true` and confirming it works. It turns rotating a profile key is more involved and might require registering a new account according to Matthew. commitc08058ee4b
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 16:32:24 2018 -0500 Convert `FileList` to `Array` commit70a6c42019
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 15:46:34 2018 -0500 🎨 Fix lint errors commit2ca7cdbc31
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 15:07:09 2018 -0500 Skip `autoOrientImage` for non-JPEG images commit58eac38301
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 14:55:35 2018 -0500 Move new-style modules to `window.Signal` namespace commit02c9328877
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 14:35:23 2018 -0500 Extract `npm run test-modules` command commit2c708eb94f
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:25:51 2018 -0500 Extract `Message.process` commit4a2e52f68a
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:25:12 2018 -0500 Fix EditorConfig commita346bab5db
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:13:02 2018 -0500 Remove `vim` directives on ESLint-ed files commit7ec885c635
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:08:24 2018 -0500 Remove CSP whitelisting of `blob:` We no longer use `autoOrientImage` using blob URLs. Bring this back if we decide to auto-orient legacy attachments. commit879b6f58f4
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:57:05 2018 -0500 Use `Message` type to determine send function Throws on invalid message type. commit5203d945c9
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:56:48 2018 -0500 Whitelist `Whisper` global commit8ad0b066a3
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:56:32 2018 -0500 Add `Whisper.Types` namespace This avoids namespace collision for `Whisper.Message`. commit785a949fce
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:55:43 2018 -0500 Add `Message` type commit674a7357ab
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:35:23 2018 -0500 Run ESLint on `Conversation::sendMessage` commitcd985aa700
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:34:38 2018 -0500 Document type signature of `FileInputView::readFile` commitd70d70e52c
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:31:16 2018 -0500 Move attachment processing closer to sending This helps ensure processing happens uniformly, regardless of which code paths are taken to send an attachment. commit532ac3e273
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:22:29 2018 -0500 Process attachment before it’s sent Picked this place since it already had various async steps, similar to `onMessageReceived` for the incoming `Attachment.process`. Could we try have this live closer to where we store it in IndexedDB, e.g. `Conversation::sendMessage`? commita4582ae2fb
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:21:42 2018 -0500 Refactor `getFile` and `getFiles` Lint them using ESLint. commit07e9114e65
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:37:31 2018 -0500 Document incoming and outgoing attachments fields Note how outgoing message attachments only have 4 fields. This presumably means the others are not used in our code and could be discarded for simplicity. commitfdc3ef289d
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:36:21 2018 -0500 Highlight that `dataURLToBlob` is synchronous commitb9c6bf600f
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:35:49 2018 -0500 Add EditorConfig configuration commite56101e229
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:34:23 2018 -0500 Replace custom with `blob-util` functions IMPORTANT: All of them are async so we need to use `await`, otherwise we get strange or silent errors. commitf95150f6a9
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:17:30 2018 -0500 Revert "Replace custom functions with `blob-util`" This reverts commit 8a81e9c01bfe80c0e1bf76737092206c06949512. commit33860d93f3
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:13:02 2018 -0500 Revert "Replace `blueimp-canvas-to-blob` with `blob-util`" This reverts commit 31b3e853e4afc78fe80995921aa4152d9f6e4783. commit7a0ba6fed6
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:12:58 2018 -0500 Replace `blueimp-canvas-to-blob` with `blob-util` commit47a5f2bfd8
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:55:34 2018 -0500 Replace custom functions with `blob-util` commit1cfa0efdb4
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:47:02 2018 -0500 Add `blob-util` dependency commit9ac26be1bd
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:46:44 2018 -0500 Document why we drop original image data during auto-orient commit4136d6c382
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:46:27 2018 -0500 Extract `DEFAULT_JPEG_QUALITY` commit4a7156327e
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:37:11 2018 -0500 Drop support for invalid `image/jpg` MIME type commit69fe96581f
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:54:30 2018 -0500 Document `window.onInvalidStateError` global commita48ba1c774
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:54:04 2018 -0500 Selectively run ESLint on `js/background.js` Enabling ESLint on a per function basis allows us to incrementally improve the codebase without requiring large and potentially risky refactorings. commite6d1cf826b
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:16:23 2018 -0500 Move async attachment processing to `onMessageReceived` We previously processed attachments in `handleDataMessage` which is mostly a synchronous function, except for the saving of the model. Moving the processing into the already async `onMessageReceived` improves code clarity. commitbe6ca2a9aa
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:14:49 2018 -0500 Document import of ES2015+ modules commiteaaf7c4160
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:14:29 2018 -0500 🎨 Fix lint error commita25b0e2e3d
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:13:57 2018 -0500 🎨 Organize `require`s commite0cc3d8fab
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:07:17 2018 -0500 Implement attachment process version Instead of keeping track of last normalization (processing) date, we now keep track of an internal processing version that will help us understand what kind of processing has already been completed for a given attachment. This will let us retroactively upgrade existing attachments. As we add more processing steps, we can build a processing pipeline that can convert any attachment processing version into a higher one, e.g. 4 -> 5 -> 6 -> 7. commitad9083d0fd
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:50:31 2018 -0500 Ignore ES2015+ files during JSCS linting commit96641205f7
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:48:07 2018 -0500 Improve ESLint ignore rules Apparently, using unqualified `/**` patterns prevents `!` include patterns. Using qualified glob patterns, e.g. `js/models/**/*.js`, lets us work around this. commit255e0ab15b
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:44:59 2018 -0500 🔤 ESLint ignored files commitebcb70258a
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:35:47 2018 -0500 Whitelist `browser` environment for ESLint commit3eaace6f3a
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:35:05 2018 -0500 Use `MIME` module commitba2cf7770e
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:32:54 2018 -0500 🎨 Fix lint errors commit65acc86e85
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:30:42 2018 -0500 Add ES2015+ files to JSHint ignored list commit8b6494ae6c
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:29:20 2018 -0500 Document potentially unexpected `autoScale` behavior commit8b4c69b200
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:26:47 2018 -0500 Test CommonJS modules separately Not sure how to test them as part of Grunt `unit-tests` task as `test/index.html` doesn’t allow for inclusion of CommonJS modules that use `require`. The tests are silently skipped. commit213400e4b2
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:24:27 2018 -0500 Add `MIME` type module commit37a726e4fb
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:18:05 2018 -0500 Return proper `Error` from `blobArrayToBuffer` commit164752db56
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:15:41 2018 -0500 🎨 Fix ESLint errors commitd498dd79a0
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:14:33 2018 -0500 Update `Attachment` type field definitions commit141155a153
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:12:50 2018 -0500 Move `blueimp-canvas-to-blob` from Bower to npm commit7ccb833e5d
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:33:50 2018 -0500 🎨 Clarify data flow commite7da41591f
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:31:21 2018 -0500 Use `blobUrl` for consistency commit523a80eefe
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:28:06 2018 -0500 Remove just-in-time image auto-orient for lightbox We can bring this back if our users would like auto-orient for old attachments. commit0739feae9c
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:27:21 2018 -0500 Remove just-in-time auto-orient of message attachments We can bring this back if our users would like auto-orient for old attachments. But better yet, we might implement this as database migration. commited43c66f92
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:26:24 2018 -0500 Auto-orient JPEG attachments upon receipt commite2eb8e36b0
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:25:26 2018 -0500 Expose `Attachment` type through `Whisper.Attachment` commit9638fbc987
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:23:39 2018 -0500 Use `contentType` from `model` commit032c0ced46
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:23:04 2018 -0500 Return `Error` object for `autoOrientImage` failures commitff04bad851
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:22:32 2018 -0500 Add `options` for `autoOrientImage` output type / quality commit87745b5586
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:18:46 2018 -0500 Add `Attachment` type Defines various functions on attachments, e.g. normalization (auto-orient JPEGs, etc.) commitde27fdc10a
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:16:34 2018 -0500 Add `yarn grunt` shortcut This allows us to use local `grunt-cli` for `grunt dev`. commit59974db5a5
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 10:10:11 2018 -0500 Improve readability commitb5ba96f1e6
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 10:08:12 2018 -0500 Use `snake_case` for module names Prevents problems across case-sensitive and case-insensitive file systems. We can work around this in the future using a lint rule such as `eslint-plugin-require-path-exists`. See discussion: https://github.com/signalapp/Signal-Desktop/pull/2040#discussion_r167365931 commit48c5d3155c
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 10:05:44 2018 -0500 🎨 Use destructuring commit4822f49f22
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:41:40 2018 -0500 Auto-orient images in lightbox view commit7317110809
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:40:14 2018 -0500 Document magic number for escape key commitc790d07389
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:38:35 2018 -0500 Make second `View` argument an `options` object commitfbe010bb63
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:27:40 2018 -0500 Allow `loadImage` to fetch `blob://` URLs commitec35710d00
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:57:48 2018 -0500 🎨 Shorten `autoOrientImage` import commitd07433e3cf
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:57:19 2018 -0500 Make `autoOrientImage` module standalone commitc285bf5e33
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:55:44 2018 -0500 Replace `loadImage` with `autoOrientImage` commit4431854923
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:53:23 2018 -0500 Add `autoOrientImage` module This module exposes `loadImage` with a `Promise` based interface and pre- populates `orientation: true` option to auto-orient input. Returns data URL as string. The module uses a named export as refactoring references of modules with `default` (`module.exports`) export references can be error-prone. See: https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html commitc77063afc6
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:44:30 2018 -0500 Auto-orient preview images See: #998 commit06dba5eb8f
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:43:23 2018 -0500 TODO: Use native `Canvas::toBlob` One challenge is that `Canvas::toBlob` is async whereas `dataURLtoBlob` is sync. commitb15c304a31
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:42:45 2018 -0500 Make `null` check strict Appeases JSHint. ESLint has a nice `smart` option for `eqeqeq` rule: https://eslint.org/docs/rules/eqeqeq#smart commitea70b92d9b
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 15:23:58 2018 -0500 Use `Canvas::toDataURL` to preserve `ImageView` logic This way, all the other code paths remain untouched in case we want to remove the auto-orient code once Chrome supports the `image-orientation` CSS property. See: - #998 - https://developer.mozilla.org/en-US/docs/Web/CSS/image-orientation commit62fd744f9f
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:38:04 2018 -0500 Use CSS to constrain auto-oriented images commitf4d3392687
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:35:02 2018 -0500 Replace `ImageView` `el` with auto-oriented `canvas` See: #998 commit1602d7f610
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:25:48 2018 -0500 Pass `Blob` to `View` (for `ImageView`) This allows us to do JPEG auto-orientation based on EXIF metadata. commite6a414f2b2
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:25:12 2018 -0500 🔪 Remove newline commit5f0d9570d7
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 11:17:02 2018 -0500 Expose `blueimp-load-image` as `window.loadImage` commit1e1c62fe2f
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 11:16:46 2018 -0500 Add `blueimp-load-image` npm dependency commitad17fa8a68
Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 11:14:40 2018 -0500 Remove `blueimp-load-image` Bower dependency
This commit is contained in:
parent
be5cbc9d2b
commit
a0da73ca8d
|
@ -0,0 +1,14 @@
|
|||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{js/modules/**/*.js, test/modules/**/*.js}]
|
||||
indent_size = 2
|
|
@ -1,17 +1,23 @@
|
|||
build/**
|
||||
components/**
|
||||
coverage/**
|
||||
dist/**
|
||||
libtextsecure/**
|
||||
coverage/**
|
||||
|
||||
# these aren't ready yet, pulling files in one-by-one
|
||||
js/**
|
||||
test/**
|
||||
js/*.js
|
||||
js/models/**/*.js
|
||||
js/react/**/*.js
|
||||
js/views/**/*.js
|
||||
test/*.js
|
||||
test/models/*.js
|
||||
test/views/*.js
|
||||
/*.js
|
||||
|
||||
# ES2015+ files
|
||||
!js/background.js
|
||||
!js/models/conversations.js
|
||||
!js/views/file_input_view.js
|
||||
!js/views/attachment_view.js
|
||||
!main.js
|
||||
!prepare_build.js
|
||||
|
||||
# all of these files will be new
|
||||
!test/server/**/*.js
|
||||
|
||||
# all of app/ is included
|
||||
|
|
|
@ -21,7 +21,7 @@ module.exports = {
|
|||
}],
|
||||
|
||||
// putting params on their own line helps stay within line length limit
|
||||
'function-paren-newline': ['error', 'consistent'],
|
||||
'function-paren-newline': ['error', 'multiline'],
|
||||
|
||||
// 90 characters allows three+ side-by-side screens on a standard-size monitor
|
||||
'max-len': ['error', {
|
||||
|
@ -37,5 +37,7 @@ module.exports = {
|
|||
|
||||
// though we have a logger, we still remap console to log to disk
|
||||
'no-console': 'off',
|
||||
|
||||
'operator-linebreak': 'error',
|
||||
}
|
||||
};
|
||||
|
|
|
@ -103,6 +103,7 @@ module.exports = function(grunt) {
|
|||
'!js/Mp3LameEncoder.min.js',
|
||||
'!js/libsignal-protocol-worker.js',
|
||||
'!js/components.js',
|
||||
'!js/modules/**/*.js',
|
||||
'!js/signal_protocol_store.js',
|
||||
'_locales/**/*'
|
||||
],
|
||||
|
@ -174,8 +175,10 @@ module.exports = function(grunt) {
|
|||
'!js/Mp3LameEncoder.min.js',
|
||||
'!js/libsignal-protocol-worker.js',
|
||||
'!js/components.js',
|
||||
'!js/modules/**/*.js',
|
||||
'test/**/*.js',
|
||||
'!test/blanket_mocha.js',
|
||||
'!test/modules/**/*.js',
|
||||
'!test/test.js',
|
||||
]
|
||||
}
|
||||
|
|
|
@ -117,8 +117,8 @@ function eliminateOutOfDateFiles(logPath, date) {
|
|||
const file = {
|
||||
path: target,
|
||||
start: isLineAfterDate(start, date),
|
||||
end: isLineAfterDate(end[end.length - 1], date)
|
||||
|| isLineAfterDate(end[end.length - 2], date),
|
||||
end: isLineAfterDate(end[end.length - 1], date) ||
|
||||
isLineAfterDate(end[end.length - 2], date),
|
||||
};
|
||||
|
||||
if (!file.start && !file.end) {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none';
|
||||
connect-src 'self' wss: https:;
|
||||
connect-src 'self' https: wss:;
|
||||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' blob: data:;
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"indexeddb-backbonejs-adapter": "*",
|
||||
"intl-tel-input": "~4.0.1",
|
||||
"blueimp-load-image": "~1.13.0",
|
||||
"blueimp-canvas-to-blob": "~2.1.1",
|
||||
"autosize": "~4.0.0",
|
||||
"webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git",
|
||||
"mp3lameencoder": "https://github.com/higuma/mp3-lame-encoder-js.git",
|
||||
|
@ -69,12 +68,6 @@
|
|||
"build/img/flags.png",
|
||||
"build/js/intlTelInput.js"
|
||||
],
|
||||
"blueimp-load-image": [
|
||||
"js/load-image.js"
|
||||
],
|
||||
"blueimp-canvas-to-blob": [
|
||||
"js/canvas-to-blob.js"
|
||||
],
|
||||
"emojijs": [
|
||||
"lib/emoji.js",
|
||||
"demo/emoji.css"
|
||||
|
@ -113,8 +106,6 @@
|
|||
"moment",
|
||||
"intl-tel-input",
|
||||
"backbone.typeahead",
|
||||
"blueimp-load-image",
|
||||
"blueimp-canvas-to-blob",
|
||||
"autosize",
|
||||
"filesize"
|
||||
],
|
||||
|
|
194
js/background.js
194
js/background.js
|
@ -1,9 +1,25 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
/* eslint-disable */
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
/* global Backbone: false */
|
||||
/* global $: false */
|
||||
|
||||
/* global ConversationController: false */
|
||||
/* global getAccountManager: false */
|
||||
/* global Signal: false */
|
||||
/* global storage: false */
|
||||
/* global textsecure: false */
|
||||
/* global Whisper: false */
|
||||
/* global wrapDeferred: false */
|
||||
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
const { Message } = window.Signal.Types;
|
||||
|
||||
// Implicitly used in `indexeddb-backbonejs-adapter`:
|
||||
// https://github.com/signalapp/Signal-Desktop/blob/4033a9f8137e62ed286170ed5d4941982b1d3a64/components/indexeddb-backbonejs-adapter/backbone-indexeddb.js#L569
|
||||
window.onInvalidStateError = function(e) {
|
||||
console.log(e);
|
||||
};
|
||||
|
@ -479,84 +495,118 @@
|
|||
});
|
||||
}
|
||||
|
||||
function onMessageReceived(ev) {
|
||||
var data = ev.data;
|
||||
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
||||
var profileKey = data.message.profileKey.toArrayBuffer();
|
||||
return ConversationController.getOrCreateAndWait(data.source, 'private').then(function(sender) {
|
||||
return sender.setProfileKey(profileKey).then(ev.confirm);
|
||||
});
|
||||
}
|
||||
var message = initIncomingMessage(data);
|
||||
/* eslint-enable */
|
||||
/* jshint ignore:start */
|
||||
|
||||
return isMessageDuplicate(message).then(function(isDuplicate) {
|
||||
if (isDuplicate) {
|
||||
console.log('Received duplicate message', message.idForLogging());
|
||||
ev.confirm();
|
||||
return;
|
||||
}
|
||||
// Descriptors
|
||||
const getGroupDescriptor = group => ({
|
||||
type: Message.GROUP,
|
||||
id: group.id,
|
||||
});
|
||||
|
||||
var type, id;
|
||||
if (data.message.group) {
|
||||
type = 'group';
|
||||
id = data.message.group.id;
|
||||
} else {
|
||||
type = 'private';
|
||||
id = data.source;
|
||||
}
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
||||
const getDescriptorForSent = ({ message, destination }) => (
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: { type: Message.PRIVATE, id: destination }
|
||||
);
|
||||
|
||||
return ConversationController.getOrCreateAndWait(id, type).then(function() {
|
||||
return message.handleDataMessage(data.message, ev.confirm, {
|
||||
initialLoadComplete: initialLoadComplete
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||
const getDescriptorForReceived = ({ message, source }) => (
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: { type: Message.PRIVATE, id: source }
|
||||
);
|
||||
|
||||
function onSentMessage(ev) {
|
||||
var now = new Date().getTime();
|
||||
var data = ev.data;
|
||||
function createMessageHandler({
|
||||
createMessage,
|
||||
getMessageDescriptor,
|
||||
handleProfileUpdate,
|
||||
}) {
|
||||
return async (event) => {
|
||||
const { data, confirm } = event;
|
||||
|
||||
var type, id;
|
||||
if (data.message.group) {
|
||||
type = 'group';
|
||||
id = data.message.group.id;
|
||||
} else {
|
||||
type = 'private';
|
||||
id = data.destination;
|
||||
}
|
||||
const messageDescriptor = getMessageDescriptor(data);
|
||||
|
||||
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
||||
return ConversationController.getOrCreateAndWait(id, type).then(function(convo) {
|
||||
return convo.save({profileSharing: true}).then(ev.confirm);
|
||||
});
|
||||
}
|
||||
const { PROFILE_KEY_UPDATE } = textsecure.protobuf.DataMessage.Flags;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
|
||||
if (isProfileUpdate) {
|
||||
return handleProfileUpdate({ data, confirm, messageDescriptor });
|
||||
}
|
||||
|
||||
var message = new Whisper.Message({
|
||||
source : textsecure.storage.user.getNumber(),
|
||||
sourceDevice : data.device,
|
||||
sent_at : data.timestamp,
|
||||
received_at : now,
|
||||
conversationId : data.destination,
|
||||
type : 'outgoing',
|
||||
sent : true,
|
||||
expirationStartTimestamp: data.expirationStartTimestamp,
|
||||
});
|
||||
const message = createMessage(data);
|
||||
const isDuplicate = await isMessageDuplicate(message);
|
||||
if (isDuplicate) {
|
||||
console.log('Received duplicate message', message.idForLogging());
|
||||
return event.confirm();
|
||||
}
|
||||
|
||||
return isMessageDuplicate(message).then(function(isDuplicate) {
|
||||
if (isDuplicate) {
|
||||
console.log('Received duplicate message', message.idForLogging());
|
||||
ev.confirm();
|
||||
return;
|
||||
}
|
||||
const upgradedMessage = await Message.upgradeSchema(data.message);
|
||||
await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
);
|
||||
return message.handleDataMessage(
|
||||
upgradedMessage,
|
||||
event.confirm,
|
||||
{ initialLoadComplete }
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return ConversationController.getOrCreateAndWait(id, type).then(function() {
|
||||
return message.handleDataMessage(data.message, ev.confirm, {
|
||||
initialLoadComplete: initialLoadComplete
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// Received:
|
||||
async function handleMessageReceivedProfileUpdate({
|
||||
data,
|
||||
confirm,
|
||||
messageDescriptor,
|
||||
}) {
|
||||
const profileKey = data.message.profileKey.toArrayBuffer();
|
||||
const sender = await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
'private'
|
||||
);
|
||||
await sender.setProfileKey(profileKey);
|
||||
return confirm();
|
||||
}
|
||||
|
||||
const onMessageReceived = createMessageHandler({
|
||||
handleProfileUpdate: handleMessageReceivedProfileUpdate,
|
||||
getMessageDescriptor: getDescriptorForReceived,
|
||||
createMessage: initIncomingMessage,
|
||||
});
|
||||
|
||||
// Sent:
|
||||
async function handleMessageSentProfileUpdate({ confirm, messageDescriptor }) {
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
);
|
||||
await conversation.save({ profileSharing: true });
|
||||
return confirm();
|
||||
}
|
||||
|
||||
function createSentMessage(data) {
|
||||
const now = Date.now();
|
||||
return new Whisper.Message({
|
||||
source: textsecure.storage.user.getNumber(),
|
||||
sourceDevice: data.device,
|
||||
sent_at: data.timestamp,
|
||||
received_at: now,
|
||||
conversationId: data.destination,
|
||||
type: 'outgoing',
|
||||
sent: true,
|
||||
expirationStartTimestamp: data.expirationStartTimestamp,
|
||||
});
|
||||
}
|
||||
|
||||
const onSentMessage = createMessageHandler({
|
||||
handleProfileUpdate: handleMessageSentProfileUpdate,
|
||||
getMessageDescriptor: getDescriptorForSent,
|
||||
createMessage: createSentMessage,
|
||||
});
|
||||
/* jshint ignore:end */
|
||||
/* eslint-disable */
|
||||
|
||||
function isMessageDuplicate(message) {
|
||||
return new Promise(function(resolve) {
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
/* eslint-disable */
|
||||
|
||||
/* global Signal: false */
|
||||
/* global storage: false */
|
||||
/* global textsecure: false */
|
||||
/* global Whisper: false */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { Attachment, Message } = window.Signal.Types;
|
||||
|
||||
// TODO: Factor out private and group subclasses of Conversation
|
||||
|
||||
var COLORS = [
|
||||
|
@ -598,54 +604,71 @@
|
|||
}
|
||||
},
|
||||
|
||||
sendMessage: function(body, attachments) {
|
||||
this.queueJob(function() {
|
||||
var now = Date.now();
|
||||
/* jshint ignore:start */
|
||||
/* eslint-enable */
|
||||
sendMessage(body, attachments) {
|
||||
this.queueJob(async () => {
|
||||
const now = Date.now();
|
||||
|
||||
console.log(
|
||||
'Sending message to conversation',
|
||||
this.idForLogging(),
|
||||
'with timestamp',
|
||||
now
|
||||
);
|
||||
console.log(
|
||||
'Sending message to conversation',
|
||||
this.idForLogging(),
|
||||
'with timestamp',
|
||||
now
|
||||
);
|
||||
|
||||
var message = this.messageCollection.add({
|
||||
body : body,
|
||||
conversationId : this.id,
|
||||
type : 'outgoing',
|
||||
attachments : attachments,
|
||||
sent_at : now,
|
||||
received_at : now,
|
||||
expireTimer : this.get('expireTimer'),
|
||||
recipients : this.getRecipients()
|
||||
});
|
||||
if (this.isPrivate()) {
|
||||
message.set({destination: this.id});
|
||||
}
|
||||
message.save();
|
||||
const upgradedAttachments =
|
||||
await Promise.all(attachments.map(Attachment.upgradeSchema));
|
||||
const message = this.messageCollection.add({
|
||||
body,
|
||||
conversationId: this.id,
|
||||
type: 'outgoing',
|
||||
attachments: upgradedAttachments,
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
expireTimer: this.get('expireTimer'),
|
||||
recipients: this.getRecipients(),
|
||||
});
|
||||
if (this.isPrivate()) {
|
||||
message.set({ destination: this.id });
|
||||
}
|
||||
message.save();
|
||||
|
||||
this.save({
|
||||
active_at : now,
|
||||
timestamp : now,
|
||||
lastMessage : message.getNotificationText()
|
||||
});
|
||||
this.save({
|
||||
active_at: now,
|
||||
timestamp: now,
|
||||
lastMessage: message.getNotificationText(),
|
||||
});
|
||||
|
||||
var sendFunc;
|
||||
if (this.get('type') == 'private') {
|
||||
sendFunc = textsecure.messaging.sendMessageToNumber;
|
||||
}
|
||||
else {
|
||||
sendFunc = textsecure.messaging.sendMessageToGroup;
|
||||
}
|
||||
const conversationType = this.get('type');
|
||||
const sendFunc = (() => {
|
||||
switch (conversationType) {
|
||||
case Message.PRIVATE:
|
||||
return textsecure.messaging.sendMessageToNumber;
|
||||
case Message.GROUP:
|
||||
return textsecure.messaging.sendMessageToGroup;
|
||||
default:
|
||||
throw new TypeError(`Invalid conversation type: '${conversationType}'`);
|
||||
}
|
||||
})();
|
||||
|
||||
var profileKey;
|
||||
if (this.get('profileSharing')) {
|
||||
profileKey = storage.get('profileKey');
|
||||
}
|
||||
let profileKey;
|
||||
if (this.get('profileSharing')) {
|
||||
profileKey = storage.get('profileKey');
|
||||
}
|
||||
|
||||
message.send(sendFunc(this.get('id'), body, attachments, now, this.get('expireTimer'), profileKey));
|
||||
}.bind(this));
|
||||
message.send(sendFunc(
|
||||
this.get('id'),
|
||||
body,
|
||||
upgradedAttachments,
|
||||
now,
|
||||
this.get('expireTimer'),
|
||||
profileKey
|
||||
));
|
||||
});
|
||||
},
|
||||
/* jshint ignore:end */
|
||||
/* eslint-disable */
|
||||
|
||||
updateLastMessage: function() {
|
||||
var collection = new Whisper.MessageCollection();
|
||||
|
|
|
@ -373,7 +373,7 @@
|
|||
// 1. on an incoming message
|
||||
// 2. on a sent message sync'd from another device
|
||||
// 3. in rare cases, an incoming message can be retried, though it will
|
||||
// still through one of the previous two codepaths.
|
||||
// still go through one of the previous two codepaths
|
||||
var message = this;
|
||||
var source = message.get('source');
|
||||
var type = message.get('type');
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
const loadImage = require('blueimp-load-image');
|
||||
|
||||
const DEFAULT_JPEG_QUALITY = 0.85;
|
||||
|
||||
// File | Blob | URLString -> LoadImageOptions -> Promise<DataURLString>
|
||||
//
|
||||
// Documentation for `options` (`LoadImageOptions`):
|
||||
// https://github.com/blueimp/JavaScript-Load-Image/tree/v2.18.0#options
|
||||
exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
|
||||
const optionsWithDefaults = Object.assign(
|
||||
{
|
||||
type: 'image/jpeg',
|
||||
quality: DEFAULT_JPEG_QUALITY,
|
||||
},
|
||||
options,
|
||||
{
|
||||
canvas: true,
|
||||
orientation: true,
|
||||
}
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
loadImage(fileOrBlobOrURL, (canvasOrError) => {
|
||||
if (canvasOrError.type === 'error') {
|
||||
const error = new Error('autoOrientImage: Failed to process image');
|
||||
error.cause = canvasOrError;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasOrError;
|
||||
const dataURL = canvas.toDataURL(
|
||||
optionsWithDefaults.type,
|
||||
optionsWithDefaults.quality
|
||||
);
|
||||
|
||||
resolve(dataURL);
|
||||
}, optionsWithDefaults);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
const MIME = require('./mime');
|
||||
const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util');
|
||||
const { autoOrientImage } = require('../auto_orient_image');
|
||||
|
||||
// Increment this everytime we change how attachments are upgraded. This allows us to
|
||||
// retroactively upgrade existing attachments. As we add more upgrade steps, we could
|
||||
// design a pipeline that does this incrementally, e.g. from version 0 (unknown) -> 1,
|
||||
// 1 --> 2, etc., similar to how we do database migrations:
|
||||
const CURRENT_PROCESS_VERSION = 1;
|
||||
|
||||
// Schema version history
|
||||
//
|
||||
// Version 1
|
||||
// - Auto-orient JPEG attachments using EXIF `Orientation` data
|
||||
// - Add `schemaVersion` property
|
||||
|
||||
// // Incoming message attachment fields
|
||||
// {
|
||||
// id: string
|
||||
// contentType: MIMEType
|
||||
// data: ArrayBuffer
|
||||
// digest: ArrayBuffer
|
||||
// fileName: string
|
||||
// flags: null
|
||||
// key: ArrayBuffer
|
||||
// size: integer
|
||||
// thumbnail: ArrayBuffer
|
||||
// schemaVersion: integer
|
||||
// }
|
||||
|
||||
// // Outgoing message attachment fields
|
||||
// {
|
||||
// contentType: MIMEType
|
||||
// data: ArrayBuffer
|
||||
// fileName: string
|
||||
// size: integer
|
||||
// schemaVersion: integer
|
||||
// }
|
||||
|
||||
// Middleware
|
||||
// type UpgradeStep = Attachment -> Promise Attachment
|
||||
|
||||
// UpgradeStep -> SchemaVersion -> UpgradeStep
|
||||
const setSchemaVersion = (next, schemaVersion) => async (attachment) => {
|
||||
const isAlreadyUpgraded = attachment.schemaVersion >= schemaVersion;
|
||||
if (isAlreadyUpgraded) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
let upgradedAttachment;
|
||||
try {
|
||||
upgradedAttachment = await next(attachment);
|
||||
} catch (error) {
|
||||
console.error('Attachment.setSchemaVersion: error:', error);
|
||||
upgradedAttachment = null;
|
||||
}
|
||||
|
||||
const hasSuccessfullyUpgraded = upgradedAttachment !== null;
|
||||
if (!hasSuccessfullyUpgraded) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
upgradedAttachment,
|
||||
{ schemaVersion }
|
||||
);
|
||||
};
|
||||
|
||||
// Upgrade steps
|
||||
const autoOrientJPEG = async (attachment) => {
|
||||
if (!MIME.isJPEG(attachment.contentType)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const dataBlob = await arrayBufferToBlob(attachment.data, attachment.contentType);
|
||||
const newDataBlob = await dataURLToBlob(await autoOrientImage(dataBlob));
|
||||
const newDataArrayBuffer = await blobToArrayBuffer(newDataBlob);
|
||||
|
||||
// IMPORTANT: We overwrite the existing `data` `ArrayBuffer` losing the original
|
||||
// image data. Ideally, we’d preserve the original image data for users who want to
|
||||
// retain it but due to reports of data loss, we don’t want to overburden IndexedDB
|
||||
// by potentially doubling stored image data.
|
||||
// See: https://github.com/signalapp/Signal-Desktop/issues/1589
|
||||
const newAttachment = Object.assign({}, attachment, {
|
||||
data: newDataArrayBuffer,
|
||||
size: newDataArrayBuffer.byteLength,
|
||||
});
|
||||
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
delete newAttachment.digest;
|
||||
|
||||
return newAttachment;
|
||||
};
|
||||
|
||||
// Public API
|
||||
// UpgradeStep
|
||||
exports.upgradeSchema = setSchemaVersion(autoOrientJPEG, CURRENT_PROCESS_VERSION);
|
|
@ -0,0 +1,17 @@
|
|||
const Attachment = require('./attachment');
|
||||
|
||||
|
||||
const GROUP = 'group';
|
||||
const PRIVATE = 'private';
|
||||
|
||||
// Public API
|
||||
exports.GROUP = GROUP;
|
||||
exports.PRIVATE = PRIVATE;
|
||||
|
||||
// Schema
|
||||
// Message -> Promise Message
|
||||
exports.upgradeSchema = async message =>
|
||||
Object.assign({}, message, {
|
||||
attachments:
|
||||
await Promise.all(message.attachments.map(Attachment.upgradeSchema)),
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
exports.isJPEG = mimeType =>
|
||||
mimeType === 'image/jpeg';
|
|
@ -1,271 +1,290 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
/* eslint-env browser */
|
||||
|
||||
/* global $: false */
|
||||
/* global _: false */
|
||||
/* global Backbone: false */
|
||||
/* global moment: false */
|
||||
|
||||
/* global i18n: false */
|
||||
/* global textsecure: false */
|
||||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
'use strict';
|
||||
const ESCAPE_KEY_CODE = 27;
|
||||
|
||||
var FileView = Whisper.View.extend({
|
||||
tagName: 'div',
|
||||
className: 'fileView',
|
||||
templateName: 'file-view',
|
||||
render_attributes: function() {
|
||||
return this.model;
|
||||
}
|
||||
const FileView = Whisper.View.extend({
|
||||
tagName: 'div',
|
||||
className: 'fileView',
|
||||
templateName: 'file-view',
|
||||
render_attributes() {
|
||||
return this.model;
|
||||
},
|
||||
});
|
||||
|
||||
var ImageView = Backbone.View.extend({
|
||||
tagName: 'img',
|
||||
initialize: function(dataUrl) {
|
||||
this.dataUrl = dataUrl;
|
||||
},
|
||||
events: {
|
||||
'load': 'update',
|
||||
},
|
||||
update: function() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render: function() {
|
||||
this.$el.attr('src', this.dataUrl);
|
||||
return this;
|
||||
}
|
||||
const ImageView = Backbone.View.extend({
|
||||
tagName: 'img',
|
||||
initialize(blobUrl) {
|
||||
this.blobUrl = blobUrl;
|
||||
},
|
||||
events: {
|
||||
load: 'update',
|
||||
},
|
||||
update() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render() {
|
||||
this.$el.attr('src', this.blobUrl);
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
var MediaView = Backbone.View.extend({
|
||||
initialize: function(dataUrl, contentType) {
|
||||
this.dataUrl = dataUrl;
|
||||
this.contentType = contentType;
|
||||
this.$el.attr('controls', '');
|
||||
},
|
||||
events: {
|
||||
'canplay': 'canplay'
|
||||
},
|
||||
canplay: function() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render: function() {
|
||||
var $el = $('<source>');
|
||||
$el.attr('src', this.dataUrl);
|
||||
this.$el.append($el);
|
||||
return this;
|
||||
}
|
||||
const MediaView = Backbone.View.extend({
|
||||
initialize(dataUrl, { contentType } = {}) {
|
||||
this.dataUrl = dataUrl;
|
||||
this.contentType = contentType;
|
||||
this.$el.attr('controls', '');
|
||||
},
|
||||
events: {
|
||||
canplay: 'canplay',
|
||||
},
|
||||
canplay() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render() {
|
||||
const $el = $('<source>');
|
||||
$el.attr('src', this.dataUrl);
|
||||
this.$el.append($el);
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
var AudioView = MediaView.extend({ tagName: 'audio' });
|
||||
var VideoView = MediaView.extend({ tagName: 'video' });
|
||||
const AudioView = MediaView.extend({ tagName: 'audio' });
|
||||
const VideoView = MediaView.extend({ tagName: 'video' });
|
||||
|
||||
// Blacklist common file types known to be unsupported in Chrome
|
||||
var UnsupportedFileTypes = [
|
||||
const UnsupportedFileTypes = [
|
||||
'audio/aiff',
|
||||
'video/quicktime'
|
||||
'video/quicktime',
|
||||
];
|
||||
|
||||
Whisper.AttachmentView = Backbone.View.extend({
|
||||
tagName: 'span',
|
||||
className: function() {
|
||||
className() {
|
||||
if (this.isImage()) {
|
||||
return 'attachment';
|
||||
} else {
|
||||
return 'attachment bubbled';
|
||||
}
|
||||
return 'attachment bubbled';
|
||||
},
|
||||
initialize(options) {
|
||||
this.blob = new Blob([this.model.data], { type: this.model.contentType });
|
||||
if (!this.model.size) {
|
||||
this.model.size = this.model.data.byteLength;
|
||||
}
|
||||
if (options.timestamp) {
|
||||
this.timestamp = options.timestamp;
|
||||
}
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.blob = new Blob([this.model.data], {type: this.model.contentType});
|
||||
if (!this.model.size) {
|
||||
this.model.size = this.model.data.byteLength;
|
||||
}
|
||||
if (options.timestamp) {
|
||||
this.timestamp = options.timestamp;
|
||||
}
|
||||
},
|
||||
events: {
|
||||
'click': 'onclick'
|
||||
click: 'onclick',
|
||||
},
|
||||
unload: function() {
|
||||
this.blob = null;
|
||||
unload() {
|
||||
this.blob = null;
|
||||
|
||||
if (this.lightBoxView) {
|
||||
this.lightBoxView.remove();
|
||||
}
|
||||
if (this.fileView) {
|
||||
this.fileView.remove();
|
||||
}
|
||||
if (this.view) {
|
||||
this.view.remove();
|
||||
}
|
||||
if (this.lightBoxView) {
|
||||
this.lightBoxView.remove();
|
||||
}
|
||||
if (this.fileView) {
|
||||
this.fileView.remove();
|
||||
}
|
||||
if (this.view) {
|
||||
this.view.remove();
|
||||
}
|
||||
|
||||
this.remove();
|
||||
this.remove();
|
||||
},
|
||||
getFileType: function() {
|
||||
switch(this.model.contentType) {
|
||||
case 'video/quicktime': return 'mov';
|
||||
default: return this.model.contentType.split('/')[1];
|
||||
}
|
||||
getFileType() {
|
||||
switch (this.model.contentType) {
|
||||
case 'video/quicktime': return 'mov';
|
||||
default: return this.model.contentType.split('/')[1];
|
||||
}
|
||||
},
|
||||
onclick: function(e) {
|
||||
if (this.isImage()) {
|
||||
this.lightBoxView = new Whisper.LightboxView({ model: this });
|
||||
this.lightBoxView.render();
|
||||
this.lightBoxView.$el.appendTo(this.el);
|
||||
this.lightBoxView.$el.trigger('show');
|
||||
onclick() {
|
||||
if (this.isImage()) {
|
||||
this.lightBoxView = new Whisper.LightboxView({ model: this });
|
||||
this.lightBoxView.render();
|
||||
this.lightBoxView.$el.appendTo(this.el);
|
||||
this.lightBoxView.$el.trigger('show');
|
||||
} else {
|
||||
this.saveFile();
|
||||
}
|
||||
},
|
||||
isVoiceMessage() {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} else {
|
||||
this.saveFile();
|
||||
}
|
||||
},
|
||||
isVoiceMessage: function() {
|
||||
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) {
|
||||
return true;
|
||||
}
|
||||
// Support for android legacy voice messages
|
||||
if (this.isAudio() && this.model.fileName === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Support for android legacy voice messages
|
||||
if (this.isAudio() && this.model.fileName === null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isAudio: function() {
|
||||
return this.model.contentType.startsWith('audio/');
|
||||
isAudio() {
|
||||
return this.model.contentType.startsWith('audio/');
|
||||
},
|
||||
isVideo: function() {
|
||||
return this.model.contentType.startsWith('video/');
|
||||
isVideo() {
|
||||
return this.model.contentType.startsWith('video/');
|
||||
},
|
||||
isImage: function() {
|
||||
var type = this.model.contentType;
|
||||
return type.startsWith('image/') && type !== 'image/tiff';
|
||||
isImage() {
|
||||
const type = this.model.contentType;
|
||||
return type.startsWith('image/') && type !== 'image/tiff';
|
||||
},
|
||||
mediaType: function() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return 'voice';
|
||||
} else if (this.isAudio()) {
|
||||
return 'audio';
|
||||
} else if (this.isVideo()) {
|
||||
return 'video';
|
||||
} else if (this.isImage()) {
|
||||
return 'image';
|
||||
}
|
||||
},
|
||||
displayName: function() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return i18n('voiceMessage');
|
||||
}
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
if (this.isAudio() || this.isVideo()) {
|
||||
return i18n('mediaMessage');
|
||||
}
|
||||
mediaType() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return 'voice';
|
||||
} else if (this.isAudio()) {
|
||||
return 'audio';
|
||||
} else if (this.isVideo()) {
|
||||
return 'video';
|
||||
} else if (this.isImage()) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
return i18n('unnamedFile');
|
||||
// NOTE: The existing code had no `return` but ESLint insists. Thought
|
||||
// about throwing an error assuming this was unreachable code but it turns
|
||||
// out that content type `image/tiff` falls through here:
|
||||
return undefined;
|
||||
},
|
||||
suggestedName: function() {
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
displayName() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return i18n('voiceMessage');
|
||||
}
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
if (this.isAudio() || this.isVideo()) {
|
||||
return i18n('mediaMessage');
|
||||
}
|
||||
|
||||
var suggestion = 'signal';
|
||||
if (this.timestamp) {
|
||||
suggestion += moment(this.timestamp).format('-YYYY-MM-DD-HHmmss');
|
||||
}
|
||||
var fileType = this.getFileType();
|
||||
if (fileType) {
|
||||
suggestion += '.' + fileType;
|
||||
}
|
||||
return suggestion;
|
||||
return i18n('unnamedFile');
|
||||
},
|
||||
saveFile: function() {
|
||||
var url = window.URL.createObjectURL(this.blob, { type: 'octet/stream' });
|
||||
var a = $('<a>').attr({ href: url, download: this.suggestedName() });
|
||||
a[0].click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
render: function() {
|
||||
if (!this.isImage()) {
|
||||
this.renderFileView();
|
||||
}
|
||||
var View;
|
||||
if (this.isImage()) {
|
||||
View = ImageView;
|
||||
} else if (this.isAudio()) {
|
||||
View = AudioView;
|
||||
} else if (this.isVideo()) {
|
||||
View = VideoView;
|
||||
}
|
||||
suggestedName() {
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
|
||||
if (!View || _.contains(UnsupportedFileTypes, this.model.contentType)) {
|
||||
this.update();
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.objectUrl) {
|
||||
this.objectUrl = window.URL.createObjectURL(this.blob);
|
||||
}
|
||||
this.view = new View(this.objectUrl, this.model.contentType);
|
||||
this.view.$el.appendTo(this.$el);
|
||||
this.listenTo(this.view, 'update', this.update);
|
||||
this.view.render();
|
||||
if (View !== ImageView) {
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 5000);
|
||||
}
|
||||
return this;
|
||||
let suggestion = 'signal';
|
||||
if (this.timestamp) {
|
||||
suggestion += moment(this.timestamp).format('-YYYY-MM-DD-HHmmss');
|
||||
}
|
||||
const fileType = this.getFileType();
|
||||
if (fileType) {
|
||||
suggestion += `.${fileType}`;
|
||||
}
|
||||
return suggestion;
|
||||
},
|
||||
onTimeout: function() {
|
||||
// Image or media element failed to load. Fall back to FileView.
|
||||
this.stopListening(this.view);
|
||||
saveFile() {
|
||||
const url = window.URL.createObjectURL(this.blob, { type: 'octet/stream' });
|
||||
const a = $('<a>').attr({ href: url, download: this.suggestedName() });
|
||||
a[0].click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
render() {
|
||||
if (!this.isImage()) {
|
||||
this.renderFileView();
|
||||
}
|
||||
let View;
|
||||
if (this.isImage()) {
|
||||
View = ImageView;
|
||||
} else if (this.isAudio()) {
|
||||
View = AudioView;
|
||||
} else if (this.isVideo()) {
|
||||
View = VideoView;
|
||||
}
|
||||
|
||||
if (!View || _.contains(UnsupportedFileTypes, this.model.contentType)) {
|
||||
this.update();
|
||||
},
|
||||
renderFileView: function() {
|
||||
this.fileView = new FileView({
|
||||
model: {
|
||||
mediaType: this.mediaType(),
|
||||
fileName: this.displayName(),
|
||||
fileSize: window.filesize(this.model.size),
|
||||
altText: i18n('clickToSave')
|
||||
}
|
||||
});
|
||||
|
||||
this.fileView.$el.appendTo(this.$el.empty());
|
||||
this.fileView.render();
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.objectUrl) {
|
||||
this.objectUrl = window.URL.createObjectURL(this.blob);
|
||||
}
|
||||
|
||||
const { blob } = this;
|
||||
const { contentType } = this.model;
|
||||
this.view = new View(this.objectUrl, { blob, contentType });
|
||||
this.view.$el.appendTo(this.$el);
|
||||
this.listenTo(this.view, 'update', this.update);
|
||||
this.view.render();
|
||||
if (View !== ImageView) {
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 5000);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
onTimeout() {
|
||||
// Image or media element failed to load. Fall back to FileView.
|
||||
this.stopListening(this.view);
|
||||
this.update();
|
||||
},
|
||||
renderFileView() {
|
||||
this.fileView = new FileView({
|
||||
model: {
|
||||
mediaType: this.mediaType(),
|
||||
fileName: this.displayName(),
|
||||
fileSize: window.filesize(this.model.size),
|
||||
altText: i18n('clickToSave'),
|
||||
},
|
||||
});
|
||||
|
||||
this.fileView.$el.appendTo(this.$el.empty());
|
||||
this.fileView.render();
|
||||
return this;
|
||||
},
|
||||
update() {
|
||||
clearTimeout(this.timeout);
|
||||
this.trigger('update');
|
||||
},
|
||||
update: function() {
|
||||
clearTimeout(this.timeout);
|
||||
this.trigger('update');
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.LightboxView = Whisper.View.extend({
|
||||
templateName: 'lightbox',
|
||||
className: 'modal lightbox',
|
||||
initialize: function() {
|
||||
this.window = window;
|
||||
this.$document = $(this.window.document);
|
||||
this.listener = this.onkeyup.bind(this);
|
||||
this.$document.on('keyup', this.listener);
|
||||
},
|
||||
events: {
|
||||
'click .save': 'save',
|
||||
'click .close': 'remove',
|
||||
'click': 'onclick'
|
||||
},
|
||||
save: function(e) {
|
||||
this.model.saveFile();
|
||||
},
|
||||
onclick: function(e) {
|
||||
var $el = this.$(e.target);
|
||||
if (!$el.hasClass('image') && !$el.closest('.controls').length ) {
|
||||
e.preventDefault();
|
||||
this.remove();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onkeyup: function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.remove();
|
||||
this.$document.off('keyup', this.listener);
|
||||
}
|
||||
},
|
||||
render_attributes: function() {
|
||||
return { url: this.model.objectUrl };
|
||||
templateName: 'lightbox',
|
||||
className: 'modal lightbox',
|
||||
initialize() {
|
||||
this.window = window;
|
||||
this.$document = $(this.window.document);
|
||||
this.listener = this.onkeyup.bind(this);
|
||||
this.$document.on('keyup', this.listener);
|
||||
},
|
||||
events: {
|
||||
'click .save': 'save',
|
||||
'click .close': 'remove',
|
||||
click: 'onclick',
|
||||
},
|
||||
save() {
|
||||
this.model.saveFile();
|
||||
},
|
||||
onclick(e) {
|
||||
const $el = this.$(e.target);
|
||||
if (!$el.hasClass('image') && !$el.closest('.controls').length) {
|
||||
e.preventDefault();
|
||||
this.remove();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
return true;
|
||||
},
|
||||
onkeyup(e) {
|
||||
if (e.keyCode === ESCAPE_KEY_CODE) {
|
||||
this.remove();
|
||||
this.$document.off('keyup', this.listener);
|
||||
}
|
||||
},
|
||||
render_attributes() {
|
||||
return { url: this.model.objectUrl };
|
||||
},
|
||||
});
|
||||
}());
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
/* eslint-disable */
|
||||
|
||||
/* global textsecure: false */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { MIME } = window.Signal.Types;
|
||||
|
||||
Whisper.FileSizeToast = Whisper.ToastView.extend({
|
||||
templateName: 'file-size-modal',
|
||||
render_attributes: function() {
|
||||
|
@ -30,6 +33,7 @@
|
|||
this.thumb = new Whisper.AttachmentPreviewView();
|
||||
this.$el.addClass('file-input');
|
||||
this.window = options.window;
|
||||
this.previewObjectUrl = null;
|
||||
},
|
||||
|
||||
events: {
|
||||
|
@ -93,7 +97,6 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// loadImage.scale -> components/blueimp-load-image
|
||||
var canvas = loadImage.scale(img, {
|
||||
canvas: true, maxWidth: maxWidth, maxHeight: maxHeight
|
||||
});
|
||||
|
@ -103,11 +106,13 @@
|
|||
var blob;
|
||||
do {
|
||||
i = i - 1;
|
||||
// dataURLtoBlob -> components/blueimp-canvas-to-blob
|
||||
blob = dataURLtoBlob(
|
||||
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
|
||||
if (quality < 0.5) {
|
||||
quality = 0.5;
|
||||
}
|
||||
|
@ -132,8 +137,14 @@
|
|||
case 'audio': this.addThumb('images/audio.svg'); break;
|
||||
case 'video': this.addThumb('images/video.svg'); break;
|
||||
case 'image':
|
||||
this.oUrl = URL.createObjectURL(file);
|
||||
this.addThumb(this.oUrl);
|
||||
if (!MIME.isJPEG(file.type)) {
|
||||
this.previewObjectUrl = URL.createObjectURL(file);
|
||||
this.addThumb(this.previewObjectUrl);
|
||||
break;
|
||||
}
|
||||
|
||||
window.autoOrientImage(file)
|
||||
.then(dataURL => this.addThumb(dataURL));
|
||||
break;
|
||||
default:
|
||||
this.addThumb('images/file.svg'); break;
|
||||
|
@ -177,30 +188,38 @@
|
|||
return files && files.length && files.length > 0;
|
||||
},
|
||||
|
||||
getFiles: function() {
|
||||
var promises = [];
|
||||
var files = this.file ? [this.file] : this.$input.prop('files');
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
promises.push(this.getFile(files[i]));
|
||||
}
|
||||
this.clearForm();
|
||||
return Promise.all(promises);
|
||||
},
|
||||
/* 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)));
|
||||
this.clearForm();
|
||||
return promise;
|
||||
},
|
||||
|
||||
getFile: function(file) {
|
||||
file = file || this.file || this.$input.prop('files')[0];
|
||||
if (file === undefined) { return Promise.resolve(); }
|
||||
var flags;
|
||||
if (this.isVoiceNote) {
|
||||
flags = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||
}
|
||||
return this.autoScale(file).then(this.readFile).then(function(attachment) {
|
||||
if (flags) {
|
||||
attachment.flags = flags;
|
||||
}
|
||||
return attachment;
|
||||
}.bind(this));
|
||||
},
|
||||
getFile(rawFile) {
|
||||
const file = rawFile || this.file || this.$input.prop('files')[0];
|
||||
if (file === undefined) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const attachmentFlags = this.isVoiceNote
|
||||
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||
: null;
|
||||
|
||||
const setFlags = flags => (attachment) => {
|
||||
const newAttachment = Object.assign({}, attachment);
|
||||
if (flags) {
|
||||
newAttachment.flags = flags;
|
||||
}
|
||||
return newAttachment;
|
||||
};
|
||||
|
||||
return this.autoScale(file)
|
||||
.then(this.readFile)
|
||||
.then(setFlags(attachmentFlags));
|
||||
},
|
||||
/* jshint ignore:end */
|
||||
/* eslint-disable */
|
||||
|
||||
getThumbnail: function() {
|
||||
// Scale and crop an image to 256px square
|
||||
|
@ -228,8 +247,7 @@
|
|||
crop: true, minWidth: size, minHeight: size
|
||||
});
|
||||
|
||||
// dataURLtoBlob -> components/blueimp-canvas-to-blob
|
||||
var blob = dataURLtoBlob(canvas.toDataURL('image/png'));
|
||||
var blob = window.dataURLToBlobSync(canvas.toDataURL('image/png'));
|
||||
|
||||
resolve(blob);
|
||||
};
|
||||
|
@ -237,6 +255,7 @@
|
|||
}).then(this.readFile);
|
||||
},
|
||||
|
||||
// File -> Promise Attachment
|
||||
readFile: function(file) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var FR = new FileReader();
|
||||
|
@ -255,10 +274,11 @@
|
|||
},
|
||||
|
||||
clearForm: function() {
|
||||
if (this.oUrl) {
|
||||
URL.revokeObjectURL(this.oUrl);
|
||||
this.oUrl = null;
|
||||
if (this.previewObjectUrl) {
|
||||
URL.revokeObjectURL(this.previewObjectUrl);
|
||||
this.previewObjectUrl = null;
|
||||
}
|
||||
|
||||
this.thumb.remove();
|
||||
this.$('.avatar').show();
|
||||
this.$el.trigger('force-resize');
|
||||
|
|
|
@ -17,7 +17,7 @@ describe("ContactBuffer", function() {
|
|||
var contactInfo = new textsecure.protobuf.ContactDetails({
|
||||
name: "Zero Cool",
|
||||
number: "+10000000000",
|
||||
avatar: { contentType: "image/jpg", length: avatarLen }
|
||||
avatar: { contentType: "image/jpeg", length: avatarLen }
|
||||
});
|
||||
var contactInfoBuffer = contactInfo.encode().toArrayBuffer();
|
||||
|
||||
|
@ -41,7 +41,7 @@ describe("ContactBuffer", function() {
|
|||
count++;
|
||||
assert.strictEqual(contact.name, "Zero Cool");
|
||||
assert.strictEqual(contact.number, "+10000000000");
|
||||
assert.strictEqual(contact.avatar.contentType, "image/jpg");
|
||||
assert.strictEqual(contact.avatar.contentType, "image/jpeg");
|
||||
assert.strictEqual(contact.avatar.length, 255);
|
||||
assert.strictEqual(contact.avatar.data.byteLength, 255);
|
||||
var avatarBytes = new Uint8Array(contact.avatar.data);
|
||||
|
@ -68,7 +68,7 @@ describe("GroupBuffer", function() {
|
|||
id: new Uint8Array([1, 3, 3, 7]).buffer,
|
||||
name: "Hackers",
|
||||
members: ['cereal', 'burn', 'phreak', 'joey'],
|
||||
avatar: { contentType: "image/jpg", length: avatarLen }
|
||||
avatar: { contentType: "image/jpeg", length: avatarLen }
|
||||
});
|
||||
var groupInfoBuffer = groupInfo.encode().toArrayBuffer();
|
||||
|
||||
|
@ -93,7 +93,7 @@ describe("GroupBuffer", function() {
|
|||
assert.strictEqual(group.name, "Hackers");
|
||||
assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer);
|
||||
assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']);
|
||||
assert.strictEqual(group.avatar.contentType, "image/jpg");
|
||||
assert.strictEqual(group.avatar.contentType, "image/jpeg");
|
||||
assert.strictEqual(group.avatar.length, 255);
|
||||
assert.strictEqual(group.avatar.data.byteLength, 255);
|
||||
var avatarBytes = new Uint8Array(group.avatar.data);
|
||||
|
|
18
main.js
18
main.js
|
@ -157,10 +157,10 @@ function isVisible(window, bounds) {
|
|||
const topClearOfUpperBound = window.y >= boundsY;
|
||||
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
|
||||
|
||||
return rightSideClearOfLeftBound
|
||||
&& leftSideClearOfRightBound
|
||||
&& topClearOfUpperBound
|
||||
&& topClearOfLowerBound;
|
||||
return rightSideClearOfLeftBound &&
|
||||
leftSideClearOfRightBound &&
|
||||
topClearOfUpperBound &&
|
||||
topClearOfLowerBound;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
|
@ -277,8 +277,8 @@ function createWindow() {
|
|||
// Emitted when the window is about to be closed.
|
||||
mainWindow.on('close', (e) => {
|
||||
// If the application is terminating, just do the default
|
||||
if (windowState.shouldQuit()
|
||||
|| config.environment === 'test' || config.environment === 'test-lib') {
|
||||
if (windowState.shouldQuit() ||
|
||||
config.environment === 'test' || config.environment === 'test-lib') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -422,9 +422,9 @@ app.on('before-quit', () => {
|
|||
app.on('window-all-closed', () => {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin'
|
||||
|| config.environment === 'test'
|
||||
|| config.environment === 'test-lib') {
|
||||
if (process.platform !== 'darwin' ||
|
||||
config.environment === 'test' ||
|
||||
config.environment === 'test-lib') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"main": "main.js",
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
|
||||
"test": "npm run eslint && npm run test-server && grunt test",
|
||||
"test": "npm run eslint && npm run test-server && grunt test && npm run test-modules",
|
||||
"lint": "grunt jshint",
|
||||
"start": "electron .",
|
||||
"asarl": "asar l release/mac/Signal.app/Contents/Resources/app.asar",
|
||||
|
@ -28,6 +28,7 @@
|
|||
"build-release": "SIGNAL_ENV=production npm run build -- --config.directories.output=release",
|
||||
"build-mas-release": "npm run build-release -- -m --config.mac.target=mas",
|
||||
"build-mas-dev": "npm run build-release -- -m --config.mac.target=mas --config.type=development",
|
||||
"grunt": "grunt",
|
||||
"prep-mac-release": "npm run build-release -- -m --dir",
|
||||
"prep-release": "npm run generate && grunt prep-release && npm run build-release && npm run build-mas-release && grunt test-release",
|
||||
"release-mac": "npm run build-release -- -m --prepackaged release/mac/Signal*.app --publish=always",
|
||||
|
@ -36,10 +37,14 @@
|
|||
"release": "npm run release-mac && npm run release-win && npm run release-lin",
|
||||
"test-server": "mocha --recursive test/server",
|
||||
"test-server-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/server",
|
||||
"test-modules": "mocha --recursive test/modules",
|
||||
"eslint": "eslint .",
|
||||
"open-coverage": "open coverage/lcov-report/index.html"
|
||||
},
|
||||
"dependencies": {
|
||||
"blob-util": "^1.3.0",
|
||||
"blueimp-canvas-to-blob": "^3.14.0",
|
||||
"blueimp-load-image": "^2.18.0",
|
||||
"bunyan": "^1.8.12",
|
||||
"config": "^1.28.1",
|
||||
"electron-config": "^1.0.0",
|
||||
|
|
12
preload.js
12
preload.js
|
@ -60,6 +60,8 @@
|
|||
window.nodeSetImmediate(function() {});
|
||||
}, 1000);
|
||||
|
||||
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
||||
window.loadImage = require('blueimp-load-image');
|
||||
window.ProxyAgent = require('proxy-agent');
|
||||
window.EmojiConvertor = require('emoji-js');
|
||||
window.emojiData = require('emoji-datasource');
|
||||
|
@ -70,6 +72,16 @@
|
|||
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
||||
window.nodeNotifier = require('node-notifier');
|
||||
|
||||
const { autoOrientImage } = require('./js/modules/auto_orient_image');
|
||||
window.autoOrientImage = autoOrientImage;
|
||||
|
||||
// ES2015+ modules
|
||||
window.Signal = window.Signal || {};
|
||||
window.Signal.Types = window.Signal.Types || {};
|
||||
window.Signal.Types.Attachment = require('./js/modules/types/attachment');
|
||||
window.Signal.Types.Message = require('./js/modules/types/message');
|
||||
window.Signal.Types.MIME = require('./js/modules/types/mime');
|
||||
|
||||
// We pull this in last, because the native module involved appears to be sensitive to
|
||||
// /tmp mounted as noexec on Linux.
|
||||
require('./js/spell_check');
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
const { assert } = require('chai');
|
||||
|
||||
const MIME = require('../../../js/modules/types/mime');
|
||||
|
||||
|
||||
describe('MIME', () => {
|
||||
describe('isJPEG', () => {
|
||||
it('should return true for `image/jpeg`', () => {
|
||||
assert.isTrue(MIME.isJPEG('image/jpeg'));
|
||||
});
|
||||
|
||||
[
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'image/jpg', // invalid MIME type: https://stackoverflow.com/a/37266399/125305
|
||||
'image/gif',
|
||||
'image/tiff',
|
||||
'application/json',
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
undefined,
|
||||
]
|
||||
.forEach((value) => {
|
||||
it(`should return false for \`${value}\``, () => {
|
||||
assert.isFalse(MIME.isJPEG(value));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
35
yarn.lock
35
yarn.lock
|
@ -437,6 +437,17 @@ bl@^1.0.0:
|
|||
dependencies:
|
||||
readable-stream "^2.0.5"
|
||||
|
||||
blob-util@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-1.3.0.tgz#dbb4e8caffd50b5720d347e1169b6369ba34fe95"
|
||||
dependencies:
|
||||
blob "0.0.4"
|
||||
native-or-lie "1.0.2"
|
||||
|
||||
blob@0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
|
||||
|
||||
block-stream@*:
|
||||
version "0.0.9"
|
||||
resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
|
||||
|
@ -457,6 +468,14 @@ bluebird@^3.5.1:
|
|||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
||||
|
||||
blueimp-canvas-to-blob@^3.14.0:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.14.0.tgz#ea075ffbfb1436607b0c75e951fb1ceb3ca0288e"
|
||||
|
||||
blueimp-load-image@^2.18.0:
|
||||
version "2.18.0"
|
||||
resolved "https://registry.yarnpkg.com/blueimp-load-image/-/blueimp-load-image-2.18.0.tgz#03b93687eb382a7136cfbcbd4f0e936b6763fc0e"
|
||||
|
||||
bmp-js@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.1.tgz#5ad0147099d13a9f38aa7b99af1d6e78666ed37f"
|
||||
|
@ -2606,6 +2625,10 @@ ignore@^3.3.3:
|
|||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
|
||||
import-lazy@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
|
||||
|
@ -3150,6 +3173,12 @@ levn@^0.3.0, levn@~0.3.0:
|
|||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
lie@*:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.2.0.tgz#4f13f2f8bbb027d383db338c43043545791aa8dc"
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
livereload-js@^2.2.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2"
|
||||
|
@ -3490,6 +3519,12 @@ nan@^2.0.0, nan@^2.3.2, nan@^2.3.3:
|
|||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
|
||||
|
||||
native-or-lie@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/native-or-lie/-/native-or-lie-1.0.2.tgz#c870ee0ba0bf0ff11350595d216cfea68a6d8086"
|
||||
dependencies:
|
||||
lie "*"
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
|
|
Loading…
Reference in New Issue