mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
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 --- commitce5090b473Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 10:35:36 2018 -0500 Inline message descriptors commit329036e59cAuthor: 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. commitf9d4cfb2baAuthor: 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. commitdb588997acAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:15:59 2018 -0500 Remove obsolete TODO commit799c881763Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:12:18 2018 -0500 Enable ESLint `function-paren-newline` `multiline` Per discussion. commitb660b6bc8eAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:10:48 2018 -0500 Use `messageDescriptor.id` not `source` commit5e7309d176Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 16:29:01 2018 -0500 Remove unnecessary `eslint-env` commit393b3da55eAuthor: 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. commitb3db0bf179Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 16:18:21 2018 -0500 Add `Message` descriptor functions commit8febf125b1Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 14:46:56 2018 -0500 Fix typo commit98d951ef77Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:22:39 2018 -0500 Remove `promises` reference commita0e9559ed5Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:22:13 2018 -0500 Fix `AttachmentView::mediaType` fall-through commit67be916a83Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:03:41 2018 -0500 Remove minor TODOs commit0af186e118Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:44:41 2018 -0500 Enable ESLint for `js/views/attachment_view.js` commit28a2dc5b8aAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:44:12 2018 -0500 Remove dynamic type checks commitf4ce36fcfcAuthor: 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. commit41b92c0a31Author: 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 commit462defbe55Author: 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. commitc08058ee4bAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 16:32:24 2018 -0500 Convert `FileList` to `Array` commit70a6c42019Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 15:46:34 2018 -0500 🎨 Fix lint errors commit2ca7cdbc31Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 15:07:09 2018 -0500 Skip `autoOrientImage` for non-JPEG images commit58eac38301Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 14:55:35 2018 -0500 Move new-style modules to `window.Signal` namespace commit02c9328877Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 14:35:23 2018 -0500 Extract `npm run test-modules` command commit2c708eb94fAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:25:51 2018 -0500 Extract `Message.process` commit4a2e52f68aAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:25:12 2018 -0500 Fix EditorConfig commita346bab5dbAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:13:02 2018 -0500 Remove `vim` directives on ESLint-ed files commit7ec885c635Author: 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. commit879b6f58f4Author: 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. commit5203d945c9Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:56:48 2018 -0500 Whitelist `Whisper` global commit8ad0b066a3Author: 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`. commit785a949fceAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:55:43 2018 -0500 Add `Message` type commit674a7357abAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:35:23 2018 -0500 Run ESLint on `Conversation::sendMessage` commitcd985aa700Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:34:38 2018 -0500 Document type signature of `FileInputView::readFile` commitd70d70e52cAuthor: 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. commit532ac3e273Author: 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`? commita4582ae2fbAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:21:42 2018 -0500 Refactor `getFile` and `getFiles` Lint them using ESLint. commit07e9114e65Author: 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. commitfdc3ef289dAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:36:21 2018 -0500 Highlight that `dataURLToBlob` is synchronous commitb9c6bf600fAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:35:49 2018 -0500 Add EditorConfig configuration commite56101e229Author: 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. commitf95150f6a9Author: 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. commit33860d93f3Author: 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. commit7a0ba6fed6Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:12:58 2018 -0500 Replace `blueimp-canvas-to-blob` with `blob-util` commit47a5f2bfd8Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:55:34 2018 -0500 Replace custom functions with `blob-util` commit1cfa0efdb4Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:47:02 2018 -0500 Add `blob-util` dependency commit9ac26be1bdAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:46:44 2018 -0500 Document why we drop original image data during auto-orient commit4136d6c382Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:46:27 2018 -0500 Extract `DEFAULT_JPEG_QUALITY` commit4a7156327eAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:37:11 2018 -0500 Drop support for invalid `image/jpg` MIME type commit69fe96581fAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:54:30 2018 -0500 Document `window.onInvalidStateError` global commita48ba1c774Author: 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. commite6d1cf826bAuthor: 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. commitbe6ca2a9aaAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:14:49 2018 -0500 Document import of ES2015+ modules commiteaaf7c4160Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:14:29 2018 -0500 🎨 Fix lint error commita25b0e2e3dAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:13:57 2018 -0500 🎨 Organize `require`s commite0cc3d8fabAuthor: 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. commitad9083d0fdAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:50:31 2018 -0500 Ignore ES2015+ files during JSCS linting commit96641205f7Author: 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. commit255e0ab15bAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:44:59 2018 -0500 🔤 ESLint ignored files commitebcb70258aAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:35:47 2018 -0500 Whitelist `browser` environment for ESLint commit3eaace6f3aAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:35:05 2018 -0500 Use `MIME` module commitba2cf7770eAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:32:54 2018 -0500 🎨 Fix lint errors commit65acc86e85Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:30:42 2018 -0500 Add ES2015+ files to JSHint ignored list commit8b6494ae6cAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:29:20 2018 -0500 Document potentially unexpected `autoScale` behavior commit8b4c69b200Author: 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. commit213400e4b2Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:24:27 2018 -0500 Add `MIME` type module commit37a726e4fbAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:18:05 2018 -0500 Return proper `Error` from `blobArrayToBuffer` commit164752db56Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:15:41 2018 -0500 🎨 Fix ESLint errors commitd498dd79a0Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:14:33 2018 -0500 Update `Attachment` type field definitions commit141155a153Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:12:50 2018 -0500 Move `blueimp-canvas-to-blob` from Bower to npm commit7ccb833e5dAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:33:50 2018 -0500 🎨 Clarify data flow commite7da41591fAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:31:21 2018 -0500 Use `blobUrl` for consistency commit523a80eefeAuthor: 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. commit0739feae9cAuthor: 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. commited43c66f92Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:26:24 2018 -0500 Auto-orient JPEG attachments upon receipt commite2eb8e36b0Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:25:26 2018 -0500 Expose `Attachment` type through `Whisper.Attachment` commit9638fbc987Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:23:39 2018 -0500 Use `contentType` from `model` commit032c0ced46Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:23:04 2018 -0500 Return `Error` object for `autoOrientImage` failures commitff04bad851Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:22:32 2018 -0500 Add `options` for `autoOrientImage` output type / quality commit87745b5586Author: 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.) commitde27fdc10aAuthor: 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`. commit59974db5a5Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 10:10:11 2018 -0500 Improve readability commitb5ba96f1e6Author: 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 commit48c5d3155cAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 10:05:44 2018 -0500 🎨 Use destructuring commit4822f49f22Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:41:40 2018 -0500 Auto-orient images in lightbox view commit7317110809Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:40:14 2018 -0500 Document magic number for escape key commitc790d07389Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:38:35 2018 -0500 Make second `View` argument an `options` object commitfbe010bb63Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:27:40 2018 -0500 Allow `loadImage` to fetch `blob://` URLs commitec35710d00Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:57:48 2018 -0500 🎨 Shorten `autoOrientImage` import commitd07433e3cfAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:57:19 2018 -0500 Make `autoOrientImage` module standalone commitc285bf5e33Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:55:44 2018 -0500 Replace `loadImage` with `autoOrientImage` commit4431854923Author: 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 commitc77063afc6Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:44:30 2018 -0500 Auto-orient preview images See: #998 commit06dba5eb8fAuthor: 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. commitb15c304a31Author: 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 commitea70b92d9bAuthor: 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 commit62fd744f9fAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:38:04 2018 -0500 Use CSS to constrain auto-oriented images commitf4d3392687Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:35:02 2018 -0500 Replace `ImageView` `el` with auto-oriented `canvas` See: #998 commit1602d7f610Author: 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. commite6a414f2b2Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:25:12 2018 -0500 🔪 Remove newline commit5f0d9570d7Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 11:17:02 2018 -0500 Expose `blueimp-load-image` as `window.loadImage` commit1e1c62fe2fAuthor: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 11:16:46 2018 -0500 Add `blueimp-load-image` npm dependency commitad17fa8a68Author: 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:
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -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/**
|
build/**
|
||||||
components/**
|
components/**
|
||||||
|
coverage/**
|
||||||
dist/**
|
dist/**
|
||||||
libtextsecure/**
|
libtextsecure/**
|
||||||
coverage/**
|
|
||||||
|
|
||||||
# these aren't ready yet, pulling files in one-by-one
|
# these aren't ready yet, pulling files in one-by-one
|
||||||
js/**
|
js/*.js
|
||||||
test/**
|
js/models/**/*.js
|
||||||
|
js/react/**/*.js
|
||||||
|
js/views/**/*.js
|
||||||
|
test/*.js
|
||||||
|
test/models/*.js
|
||||||
|
test/views/*.js
|
||||||
/*.js
|
/*.js
|
||||||
|
|
||||||
|
# ES2015+ files
|
||||||
|
!js/background.js
|
||||||
|
!js/models/conversations.js
|
||||||
|
!js/views/file_input_view.js
|
||||||
|
!js/views/attachment_view.js
|
||||||
!main.js
|
!main.js
|
||||||
!prepare_build.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
|
// 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
|
// 90 characters allows three+ side-by-side screens on a standard-size monitor
|
||||||
'max-len': ['error', {
|
'max-len': ['error', {
|
||||||
@@ -37,5 +37,7 @@ module.exports = {
|
|||||||
|
|
||||||
// though we have a logger, we still remap console to log to disk
|
// though we have a logger, we still remap console to log to disk
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
|
|
||||||
|
'operator-linebreak': 'error',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ module.exports = function(grunt) {
|
|||||||
'!js/Mp3LameEncoder.min.js',
|
'!js/Mp3LameEncoder.min.js',
|
||||||
'!js/libsignal-protocol-worker.js',
|
'!js/libsignal-protocol-worker.js',
|
||||||
'!js/components.js',
|
'!js/components.js',
|
||||||
|
'!js/modules/**/*.js',
|
||||||
'!js/signal_protocol_store.js',
|
'!js/signal_protocol_store.js',
|
||||||
'_locales/**/*'
|
'_locales/**/*'
|
||||||
],
|
],
|
||||||
@@ -174,8 +175,10 @@ module.exports = function(grunt) {
|
|||||||
'!js/Mp3LameEncoder.min.js',
|
'!js/Mp3LameEncoder.min.js',
|
||||||
'!js/libsignal-protocol-worker.js',
|
'!js/libsignal-protocol-worker.js',
|
||||||
'!js/components.js',
|
'!js/components.js',
|
||||||
|
'!js/modules/**/*.js',
|
||||||
'test/**/*.js',
|
'test/**/*.js',
|
||||||
'!test/blanket_mocha.js',
|
'!test/blanket_mocha.js',
|
||||||
|
'!test/modules/**/*.js',
|
||||||
'!test/test.js',
|
'!test/test.js',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ function eliminateOutOfDateFiles(logPath, date) {
|
|||||||
const file = {
|
const file = {
|
||||||
path: target,
|
path: target,
|
||||||
start: isLineAfterDate(start, date),
|
start: isLineAfterDate(start, date),
|
||||||
end: isLineAfterDate(end[end.length - 1], date)
|
end: isLineAfterDate(end[end.length - 1], date) ||
|
||||||
|| isLineAfterDate(end[end.length - 2], date),
|
isLineAfterDate(end[end.length - 2], date),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!file.start && !file.end) {
|
if (!file.start && !file.end) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'none';
|
content="default-src 'none';
|
||||||
connect-src 'self' wss: https:;
|
connect-src 'self' https: wss:;
|
||||||
script-src 'self';
|
script-src 'self';
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline';
|
||||||
img-src 'self' blob: data:;
|
img-src 'self' blob: data:;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"indexeddb-backbonejs-adapter": "*",
|
"indexeddb-backbonejs-adapter": "*",
|
||||||
"intl-tel-input": "~4.0.1",
|
"intl-tel-input": "~4.0.1",
|
||||||
"blueimp-load-image": "~1.13.0",
|
"blueimp-load-image": "~1.13.0",
|
||||||
"blueimp-canvas-to-blob": "~2.1.1",
|
|
||||||
"autosize": "~4.0.0",
|
"autosize": "~4.0.0",
|
||||||
"webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git",
|
"webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git",
|
||||||
"mp3lameencoder": "https://github.com/higuma/mp3-lame-encoder-js.git",
|
"mp3lameencoder": "https://github.com/higuma/mp3-lame-encoder-js.git",
|
||||||
@@ -69,12 +68,6 @@
|
|||||||
"build/img/flags.png",
|
"build/img/flags.png",
|
||||||
"build/js/intlTelInput.js"
|
"build/js/intlTelInput.js"
|
||||||
],
|
],
|
||||||
"blueimp-load-image": [
|
|
||||||
"js/load-image.js"
|
|
||||||
],
|
|
||||||
"blueimp-canvas-to-blob": [
|
|
||||||
"js/canvas-to-blob.js"
|
|
||||||
],
|
|
||||||
"emojijs": [
|
"emojijs": [
|
||||||
"lib/emoji.js",
|
"lib/emoji.js",
|
||||||
"demo/emoji.css"
|
"demo/emoji.css"
|
||||||
@@ -113,8 +106,6 @@
|
|||||||
"moment",
|
"moment",
|
||||||
"intl-tel-input",
|
"intl-tel-input",
|
||||||
"backbone.typeahead",
|
"backbone.typeahead",
|
||||||
"blueimp-load-image",
|
|
||||||
"blueimp-canvas-to-blob",
|
|
||||||
"autosize",
|
"autosize",
|
||||||
"filesize"
|
"filesize"
|
||||||
],
|
],
|
||||||
|
|||||||
194
js/background.js
194
js/background.js
@@ -1,9 +1,25 @@
|
|||||||
/*
|
/* eslint-disable */
|
||||||
* vim: ts=4:sw=4:expandtab
|
|
||||||
*/
|
/* 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() {
|
;(function() {
|
||||||
'use strict';
|
'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) {
|
window.onInvalidStateError = function(e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
};
|
};
|
||||||
@@ -479,84 +495,118 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessageReceived(ev) {
|
/* eslint-enable */
|
||||||
var data = ev.data;
|
/* jshint ignore:start */
|
||||||
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);
|
|
||||||
|
|
||||||
return isMessageDuplicate(message).then(function(isDuplicate) {
|
// Descriptors
|
||||||
if (isDuplicate) {
|
const getGroupDescriptor = group => ({
|
||||||
console.log('Received duplicate message', message.idForLogging());
|
type: Message.GROUP,
|
||||||
ev.confirm();
|
id: group.id,
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
var type, id;
|
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
||||||
if (data.message.group) {
|
const getDescriptorForSent = ({ message, destination }) => (
|
||||||
type = 'group';
|
message.group
|
||||||
id = data.message.group.id;
|
? getGroupDescriptor(message.group)
|
||||||
} else {
|
: { type: Message.PRIVATE, id: destination }
|
||||||
type = 'private';
|
);
|
||||||
id = data.source;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ConversationController.getOrCreateAndWait(id, type).then(function() {
|
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||||
return message.handleDataMessage(data.message, ev.confirm, {
|
const getDescriptorForReceived = ({ message, source }) => (
|
||||||
initialLoadComplete: initialLoadComplete
|
message.group
|
||||||
});
|
? getGroupDescriptor(message.group)
|
||||||
});
|
: { type: Message.PRIVATE, id: source }
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function onSentMessage(ev) {
|
function createMessageHandler({
|
||||||
var now = new Date().getTime();
|
createMessage,
|
||||||
var data = ev.data;
|
getMessageDescriptor,
|
||||||
|
handleProfileUpdate,
|
||||||
|
}) {
|
||||||
|
return async (event) => {
|
||||||
|
const { data, confirm } = event;
|
||||||
|
|
||||||
var type, id;
|
const messageDescriptor = getMessageDescriptor(data);
|
||||||
if (data.message.group) {
|
|
||||||
type = 'group';
|
|
||||||
id = data.message.group.id;
|
|
||||||
} else {
|
|
||||||
type = 'private';
|
|
||||||
id = data.destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
const { PROFILE_KEY_UPDATE } = textsecure.protobuf.DataMessage.Flags;
|
||||||
return ConversationController.getOrCreateAndWait(id, type).then(function(convo) {
|
// eslint-disable-next-line no-bitwise
|
||||||
return convo.save({profileSharing: true}).then(ev.confirm);
|
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
|
||||||
});
|
if (isProfileUpdate) {
|
||||||
}
|
return handleProfileUpdate({ data, confirm, messageDescriptor });
|
||||||
|
}
|
||||||
|
|
||||||
var message = new Whisper.Message({
|
const message = createMessage(data);
|
||||||
source : textsecure.storage.user.getNumber(),
|
const isDuplicate = await isMessageDuplicate(message);
|
||||||
sourceDevice : data.device,
|
if (isDuplicate) {
|
||||||
sent_at : data.timestamp,
|
console.log('Received duplicate message', message.idForLogging());
|
||||||
received_at : now,
|
return event.confirm();
|
||||||
conversationId : data.destination,
|
}
|
||||||
type : 'outgoing',
|
|
||||||
sent : true,
|
|
||||||
expirationStartTimestamp: data.expirationStartTimestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
return isMessageDuplicate(message).then(function(isDuplicate) {
|
const upgradedMessage = await Message.upgradeSchema(data.message);
|
||||||
if (isDuplicate) {
|
await ConversationController.getOrCreateAndWait(
|
||||||
console.log('Received duplicate message', message.idForLogging());
|
messageDescriptor.id,
|
||||||
ev.confirm();
|
messageDescriptor.type
|
||||||
return;
|
);
|
||||||
}
|
return message.handleDataMessage(
|
||||||
|
upgradedMessage,
|
||||||
|
event.confirm,
|
||||||
|
{ initialLoadComplete }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return ConversationController.getOrCreateAndWait(id, type).then(function() {
|
// Received:
|
||||||
return message.handleDataMessage(data.message, ev.confirm, {
|
async function handleMessageReceivedProfileUpdate({
|
||||||
initialLoadComplete: initialLoadComplete
|
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) {
|
function isMessageDuplicate(message) {
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
/*
|
/* eslint-disable */
|
||||||
* vim: ts=4:sw=4:expandtab
|
|
||||||
*/
|
/* global Signal: false */
|
||||||
|
/* global storage: false */
|
||||||
|
/* global textsecure: false */
|
||||||
|
/* global Whisper: false */
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
|
const { Attachment, Message } = window.Signal.Types;
|
||||||
|
|
||||||
// TODO: Factor out private and group subclasses of Conversation
|
// TODO: Factor out private and group subclasses of Conversation
|
||||||
|
|
||||||
var COLORS = [
|
var COLORS = [
|
||||||
@@ -598,54 +604,71 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sendMessage: function(body, attachments) {
|
/* jshint ignore:start */
|
||||||
this.queueJob(function() {
|
/* eslint-enable */
|
||||||
var now = Date.now();
|
sendMessage(body, attachments) {
|
||||||
|
this.queueJob(async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'Sending message to conversation',
|
'Sending message to conversation',
|
||||||
this.idForLogging(),
|
this.idForLogging(),
|
||||||
'with timestamp',
|
'with timestamp',
|
||||||
now
|
now
|
||||||
);
|
);
|
||||||
|
|
||||||
var message = this.messageCollection.add({
|
const upgradedAttachments =
|
||||||
body : body,
|
await Promise.all(attachments.map(Attachment.upgradeSchema));
|
||||||
conversationId : this.id,
|
const message = this.messageCollection.add({
|
||||||
type : 'outgoing',
|
body,
|
||||||
attachments : attachments,
|
conversationId: this.id,
|
||||||
sent_at : now,
|
type: 'outgoing',
|
||||||
received_at : now,
|
attachments: upgradedAttachments,
|
||||||
expireTimer : this.get('expireTimer'),
|
sent_at: now,
|
||||||
recipients : this.getRecipients()
|
received_at: now,
|
||||||
});
|
expireTimer: this.get('expireTimer'),
|
||||||
if (this.isPrivate()) {
|
recipients: this.getRecipients(),
|
||||||
message.set({destination: this.id});
|
});
|
||||||
}
|
if (this.isPrivate()) {
|
||||||
message.save();
|
message.set({ destination: this.id });
|
||||||
|
}
|
||||||
|
message.save();
|
||||||
|
|
||||||
this.save({
|
this.save({
|
||||||
active_at : now,
|
active_at: now,
|
||||||
timestamp : now,
|
timestamp: now,
|
||||||
lastMessage : message.getNotificationText()
|
lastMessage: message.getNotificationText(),
|
||||||
});
|
});
|
||||||
|
|
||||||
var sendFunc;
|
const conversationType = this.get('type');
|
||||||
if (this.get('type') == 'private') {
|
const sendFunc = (() => {
|
||||||
sendFunc = textsecure.messaging.sendMessageToNumber;
|
switch (conversationType) {
|
||||||
}
|
case Message.PRIVATE:
|
||||||
else {
|
return textsecure.messaging.sendMessageToNumber;
|
||||||
sendFunc = textsecure.messaging.sendMessageToGroup;
|
case Message.GROUP:
|
||||||
}
|
return textsecure.messaging.sendMessageToGroup;
|
||||||
|
default:
|
||||||
|
throw new TypeError(`Invalid conversation type: '${conversationType}'`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
var profileKey;
|
let profileKey;
|
||||||
if (this.get('profileSharing')) {
|
if (this.get('profileSharing')) {
|
||||||
profileKey = storage.get('profileKey');
|
profileKey = storage.get('profileKey');
|
||||||
}
|
}
|
||||||
|
|
||||||
message.send(sendFunc(this.get('id'), body, attachments, now, this.get('expireTimer'), profileKey));
|
message.send(sendFunc(
|
||||||
}.bind(this));
|
this.get('id'),
|
||||||
|
body,
|
||||||
|
upgradedAttachments,
|
||||||
|
now,
|
||||||
|
this.get('expireTimer'),
|
||||||
|
profileKey
|
||||||
|
));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
/* jshint ignore:end */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
updateLastMessage: function() {
|
updateLastMessage: function() {
|
||||||
var collection = new Whisper.MessageCollection();
|
var collection = new Whisper.MessageCollection();
|
||||||
|
|||||||
@@ -373,7 +373,7 @@
|
|||||||
// 1. on an incoming message
|
// 1. on an incoming message
|
||||||
// 2. on a sent message sync'd from another device
|
// 2. on a sent message sync'd from another device
|
||||||
// 3. in rare cases, an incoming message can be retried, though it will
|
// 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 message = this;
|
||||||
var source = message.get('source');
|
var source = message.get('source');
|
||||||
var type = message.get('type');
|
var type = message.get('type');
|
||||||
|
|||||||
40
js/modules/auto_orient_image.js
Normal file
40
js/modules/auto_orient_image.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
98
js/modules/types/attachment.js
Normal file
98
js/modules/types/attachment.js
Normal file
@@ -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);
|
||||||
17
js/modules/types/message.js
Normal file
17
js/modules/types/message.js
Normal file
@@ -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)),
|
||||||
|
});
|
||||||
2
js/modules/types/mime.js
Normal file
2
js/modules/types/mime.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
exports.isJPEG = mimeType =>
|
||||||
|
mimeType === 'image/jpeg';
|
||||||
@@ -1,271 +1,290 @@
|
|||||||
/*
|
/* eslint-env browser */
|
||||||
* vim: ts=4:sw=4:expandtab
|
|
||||||
*/
|
/* 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 () {
|
(function () {
|
||||||
'use strict';
|
const ESCAPE_KEY_CODE = 27;
|
||||||
|
|
||||||
var FileView = Whisper.View.extend({
|
const FileView = Whisper.View.extend({
|
||||||
tagName: 'div',
|
tagName: 'div',
|
||||||
className: 'fileView',
|
className: 'fileView',
|
||||||
templateName: 'file-view',
|
templateName: 'file-view',
|
||||||
render_attributes: function() {
|
render_attributes() {
|
||||||
return this.model;
|
return this.model;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var ImageView = Backbone.View.extend({
|
const ImageView = Backbone.View.extend({
|
||||||
tagName: 'img',
|
tagName: 'img',
|
||||||
initialize: function(dataUrl) {
|
initialize(blobUrl) {
|
||||||
this.dataUrl = dataUrl;
|
this.blobUrl = blobUrl;
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
'load': 'update',
|
load: 'update',
|
||||||
},
|
},
|
||||||
update: function() {
|
update() {
|
||||||
this.trigger('update');
|
this.trigger('update');
|
||||||
},
|
},
|
||||||
render: function() {
|
render() {
|
||||||
this.$el.attr('src', this.dataUrl);
|
this.$el.attr('src', this.blobUrl);
|
||||||
return this;
|
return this;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var MediaView = Backbone.View.extend({
|
const MediaView = Backbone.View.extend({
|
||||||
initialize: function(dataUrl, contentType) {
|
initialize(dataUrl, { contentType } = {}) {
|
||||||
this.dataUrl = dataUrl;
|
this.dataUrl = dataUrl;
|
||||||
this.contentType = contentType;
|
this.contentType = contentType;
|
||||||
this.$el.attr('controls', '');
|
this.$el.attr('controls', '');
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
'canplay': 'canplay'
|
canplay: 'canplay',
|
||||||
},
|
},
|
||||||
canplay: function() {
|
canplay() {
|
||||||
this.trigger('update');
|
this.trigger('update');
|
||||||
},
|
},
|
||||||
render: function() {
|
render() {
|
||||||
var $el = $('<source>');
|
const $el = $('<source>');
|
||||||
$el.attr('src', this.dataUrl);
|
$el.attr('src', this.dataUrl);
|
||||||
this.$el.append($el);
|
this.$el.append($el);
|
||||||
return this;
|
return this;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var AudioView = MediaView.extend({ tagName: 'audio' });
|
const AudioView = MediaView.extend({ tagName: 'audio' });
|
||||||
var VideoView = MediaView.extend({ tagName: 'video' });
|
const VideoView = MediaView.extend({ tagName: 'video' });
|
||||||
|
|
||||||
// Blacklist common file types known to be unsupported in Chrome
|
// Blacklist common file types known to be unsupported in Chrome
|
||||||
var UnsupportedFileTypes = [
|
const UnsupportedFileTypes = [
|
||||||
'audio/aiff',
|
'audio/aiff',
|
||||||
'video/quicktime'
|
'video/quicktime',
|
||||||
];
|
];
|
||||||
|
|
||||||
Whisper.AttachmentView = Backbone.View.extend({
|
Whisper.AttachmentView = Backbone.View.extend({
|
||||||
tagName: 'span',
|
tagName: 'span',
|
||||||
className: function() {
|
className() {
|
||||||
if (this.isImage()) {
|
if (this.isImage()) {
|
||||||
return 'attachment';
|
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: {
|
events: {
|
||||||
'click': 'onclick'
|
click: 'onclick',
|
||||||
},
|
},
|
||||||
unload: function() {
|
unload() {
|
||||||
this.blob = null;
|
this.blob = null;
|
||||||
|
|
||||||
if (this.lightBoxView) {
|
if (this.lightBoxView) {
|
||||||
this.lightBoxView.remove();
|
this.lightBoxView.remove();
|
||||||
}
|
}
|
||||||
if (this.fileView) {
|
if (this.fileView) {
|
||||||
this.fileView.remove();
|
this.fileView.remove();
|
||||||
}
|
}
|
||||||
if (this.view) {
|
if (this.view) {
|
||||||
this.view.remove();
|
this.view.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.remove();
|
this.remove();
|
||||||
},
|
},
|
||||||
getFileType: function() {
|
getFileType() {
|
||||||
switch(this.model.contentType) {
|
switch (this.model.contentType) {
|
||||||
case 'video/quicktime': return 'mov';
|
case 'video/quicktime': return 'mov';
|
||||||
default: return this.model.contentType.split('/')[1];
|
default: return this.model.contentType.split('/')[1];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onclick: function(e) {
|
onclick() {
|
||||||
if (this.isImage()) {
|
if (this.isImage()) {
|
||||||
this.lightBoxView = new Whisper.LightboxView({ model: this });
|
this.lightBoxView = new Whisper.LightboxView({ model: this });
|
||||||
this.lightBoxView.render();
|
this.lightBoxView.render();
|
||||||
this.lightBoxView.$el.appendTo(this.el);
|
this.lightBoxView.$el.appendTo(this.el);
|
||||||
this.lightBoxView.$el.trigger('show');
|
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 {
|
// Support for android legacy voice messages
|
||||||
this.saveFile();
|
if (this.isAudio() && this.model.fileName === null) {
|
||||||
}
|
return true;
|
||||||
},
|
}
|
||||||
isVoiceMessage: function() {
|
|
||||||
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support for android legacy voice messages
|
return false;
|
||||||
if (this.isAudio() && this.model.fileName === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
isAudio: function() {
|
isAudio() {
|
||||||
return this.model.contentType.startsWith('audio/');
|
return this.model.contentType.startsWith('audio/');
|
||||||
},
|
},
|
||||||
isVideo: function() {
|
isVideo() {
|
||||||
return this.model.contentType.startsWith('video/');
|
return this.model.contentType.startsWith('video/');
|
||||||
},
|
},
|
||||||
isImage: function() {
|
isImage() {
|
||||||
var type = this.model.contentType;
|
const type = this.model.contentType;
|
||||||
return type.startsWith('image/') && type !== 'image/tiff';
|
return type.startsWith('image/') && type !== 'image/tiff';
|
||||||
},
|
},
|
||||||
mediaType: function() {
|
mediaType() {
|
||||||
if (this.isVoiceMessage()) {
|
if (this.isVoiceMessage()) {
|
||||||
return 'voice';
|
return 'voice';
|
||||||
} else if (this.isAudio()) {
|
} else if (this.isAudio()) {
|
||||||
return 'audio';
|
return 'audio';
|
||||||
} else if (this.isVideo()) {
|
} else if (this.isVideo()) {
|
||||||
return 'video';
|
return 'video';
|
||||||
} else if (this.isImage()) {
|
} else if (this.isImage()) {
|
||||||
return 'image';
|
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
displayName() {
|
||||||
if (this.model.fileName) {
|
if (this.isVoiceMessage()) {
|
||||||
return this.model.fileName;
|
return i18n('voiceMessage');
|
||||||
}
|
}
|
||||||
|
if (this.model.fileName) {
|
||||||
|
return this.model.fileName;
|
||||||
|
}
|
||||||
|
if (this.isAudio() || this.isVideo()) {
|
||||||
|
return i18n('mediaMessage');
|
||||||
|
}
|
||||||
|
|
||||||
var suggestion = 'signal';
|
return i18n('unnamedFile');
|
||||||
if (this.timestamp) {
|
|
||||||
suggestion += moment(this.timestamp).format('-YYYY-MM-DD-HHmmss');
|
|
||||||
}
|
|
||||||
var fileType = this.getFileType();
|
|
||||||
if (fileType) {
|
|
||||||
suggestion += '.' + fileType;
|
|
||||||
}
|
|
||||||
return suggestion;
|
|
||||||
},
|
},
|
||||||
saveFile: function() {
|
suggestedName() {
|
||||||
var url = window.URL.createObjectURL(this.blob, { type: 'octet/stream' });
|
if (this.model.fileName) {
|
||||||
var a = $('<a>').attr({ href: url, download: this.suggestedName() });
|
return this.model.fileName;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!View || _.contains(UnsupportedFileTypes, this.model.contentType)) {
|
let suggestion = 'signal';
|
||||||
this.update();
|
if (this.timestamp) {
|
||||||
return this;
|
suggestion += moment(this.timestamp).format('-YYYY-MM-DD-HHmmss');
|
||||||
}
|
}
|
||||||
|
const fileType = this.getFileType();
|
||||||
if (!this.objectUrl) {
|
if (fileType) {
|
||||||
this.objectUrl = window.URL.createObjectURL(this.blob);
|
suggestion += `.${fileType}`;
|
||||||
}
|
}
|
||||||
this.view = new View(this.objectUrl, this.model.contentType);
|
return suggestion;
|
||||||
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: function() {
|
saveFile() {
|
||||||
// Image or media element failed to load. Fall back to FileView.
|
const url = window.URL.createObjectURL(this.blob, { type: 'octet/stream' });
|
||||||
this.stopListening(this.view);
|
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();
|
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;
|
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({
|
Whisper.LightboxView = Whisper.View.extend({
|
||||||
templateName: 'lightbox',
|
templateName: 'lightbox',
|
||||||
className: 'modal lightbox',
|
className: 'modal lightbox',
|
||||||
initialize: function() {
|
initialize() {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.$document = $(this.window.document);
|
this.$document = $(this.window.document);
|
||||||
this.listener = this.onkeyup.bind(this);
|
this.listener = this.onkeyup.bind(this);
|
||||||
this.$document.on('keyup', this.listener);
|
this.$document.on('keyup', this.listener);
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
'click .save': 'save',
|
'click .save': 'save',
|
||||||
'click .close': 'remove',
|
'click .close': 'remove',
|
||||||
'click': 'onclick'
|
click: 'onclick',
|
||||||
},
|
},
|
||||||
save: function(e) {
|
save() {
|
||||||
this.model.saveFile();
|
this.model.saveFile();
|
||||||
},
|
},
|
||||||
onclick: function(e) {
|
onclick(e) {
|
||||||
var $el = this.$(e.target);
|
const $el = this.$(e.target);
|
||||||
if (!$el.hasClass('image') && !$el.closest('.controls').length ) {
|
if (!$el.hasClass('image') && !$el.closest('.controls').length) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.remove();
|
this.remove();
|
||||||
return false;
|
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 };
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
})();
|
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 @@
|
|||||||
/*
|
/* eslint-disable */
|
||||||
* vim: ts=4:sw=4:expandtab
|
|
||||||
*/
|
/* global textsecure: false */
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
|
const { MIME } = window.Signal.Types;
|
||||||
|
|
||||||
Whisper.FileSizeToast = Whisper.ToastView.extend({
|
Whisper.FileSizeToast = Whisper.ToastView.extend({
|
||||||
templateName: 'file-size-modal',
|
templateName: 'file-size-modal',
|
||||||
render_attributes: function() {
|
render_attributes: function() {
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
this.thumb = new Whisper.AttachmentPreviewView();
|
this.thumb = new Whisper.AttachmentPreviewView();
|
||||||
this.$el.addClass('file-input');
|
this.$el.addClass('file-input');
|
||||||
this.window = options.window;
|
this.window = options.window;
|
||||||
|
this.previewObjectUrl = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
@@ -93,7 +97,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadImage.scale -> components/blueimp-load-image
|
|
||||||
var canvas = loadImage.scale(img, {
|
var canvas = loadImage.scale(img, {
|
||||||
canvas: true, maxWidth: maxWidth, maxHeight: maxHeight
|
canvas: true, maxWidth: maxWidth, maxHeight: maxHeight
|
||||||
});
|
});
|
||||||
@@ -103,11 +106,13 @@
|
|||||||
var blob;
|
var blob;
|
||||||
do {
|
do {
|
||||||
i = i - 1;
|
i = i - 1;
|
||||||
// dataURLtoBlob -> components/blueimp-canvas-to-blob
|
blob = window.dataURLToBlobSync(
|
||||||
blob = dataURLtoBlob(
|
|
||||||
canvas.toDataURL('image/jpeg', quality)
|
canvas.toDataURL('image/jpeg', quality)
|
||||||
);
|
);
|
||||||
quality = quality * maxSize / blob.size;
|
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) {
|
if (quality < 0.5) {
|
||||||
quality = 0.5;
|
quality = 0.5;
|
||||||
}
|
}
|
||||||
@@ -132,8 +137,14 @@
|
|||||||
case 'audio': this.addThumb('images/audio.svg'); break;
|
case 'audio': this.addThumb('images/audio.svg'); break;
|
||||||
case 'video': this.addThumb('images/video.svg'); break;
|
case 'video': this.addThumb('images/video.svg'); break;
|
||||||
case 'image':
|
case 'image':
|
||||||
this.oUrl = URL.createObjectURL(file);
|
if (!MIME.isJPEG(file.type)) {
|
||||||
this.addThumb(this.oUrl);
|
this.previewObjectUrl = URL.createObjectURL(file);
|
||||||
|
this.addThumb(this.previewObjectUrl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.autoOrientImage(file)
|
||||||
|
.then(dataURL => this.addThumb(dataURL));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.addThumb('images/file.svg'); break;
|
this.addThumb('images/file.svg'); break;
|
||||||
@@ -177,30 +188,38 @@
|
|||||||
return files && files.length && files.length > 0;
|
return files && files.length && files.length > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
getFiles: function() {
|
/* eslint-enable */
|
||||||
var promises = [];
|
/* jshint ignore:start */
|
||||||
var files = this.file ? [this.file] : this.$input.prop('files');
|
getFiles() {
|
||||||
for (var i = 0; i < files.length; i++) {
|
const files = this.file ? [this.file] : Array.from(this.$input.prop('files'));
|
||||||
promises.push(this.getFile(files[i]));
|
const promise = Promise.all(files.map(file => this.getFile(file)));
|
||||||
}
|
this.clearForm();
|
||||||
this.clearForm();
|
return promise;
|
||||||
return Promise.all(promises);
|
},
|
||||||
},
|
|
||||||
|
|
||||||
getFile: function(file) {
|
getFile(rawFile) {
|
||||||
file = file || this.file || this.$input.prop('files')[0];
|
const file = rawFile || this.file || this.$input.prop('files')[0];
|
||||||
if (file === undefined) { return Promise.resolve(); }
|
if (file === undefined) {
|
||||||
var flags;
|
return Promise.resolve();
|
||||||
if (this.isVoiceNote) {
|
}
|
||||||
flags = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
|
const attachmentFlags = this.isVoiceNote
|
||||||
}
|
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||||
return this.autoScale(file).then(this.readFile).then(function(attachment) {
|
: null;
|
||||||
if (flags) {
|
|
||||||
attachment.flags = flags;
|
const setFlags = flags => (attachment) => {
|
||||||
}
|
const newAttachment = Object.assign({}, attachment);
|
||||||
return attachment;
|
if (flags) {
|
||||||
}.bind(this));
|
newAttachment.flags = flags;
|
||||||
},
|
}
|
||||||
|
return newAttachment;
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.autoScale(file)
|
||||||
|
.then(this.readFile)
|
||||||
|
.then(setFlags(attachmentFlags));
|
||||||
|
},
|
||||||
|
/* jshint ignore:end */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
getThumbnail: function() {
|
getThumbnail: function() {
|
||||||
// Scale and crop an image to 256px square
|
// Scale and crop an image to 256px square
|
||||||
@@ -228,8 +247,7 @@
|
|||||||
crop: true, minWidth: size, minHeight: size
|
crop: true, minWidth: size, minHeight: size
|
||||||
});
|
});
|
||||||
|
|
||||||
// dataURLtoBlob -> components/blueimp-canvas-to-blob
|
var blob = window.dataURLToBlobSync(canvas.toDataURL('image/png'));
|
||||||
var blob = dataURLtoBlob(canvas.toDataURL('image/png'));
|
|
||||||
|
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
};
|
};
|
||||||
@@ -237,6 +255,7 @@
|
|||||||
}).then(this.readFile);
|
}).then(this.readFile);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// File -> Promise Attachment
|
||||||
readFile: function(file) {
|
readFile: function(file) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var FR = new FileReader();
|
var FR = new FileReader();
|
||||||
@@ -255,10 +274,11 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
clearForm: function() {
|
clearForm: function() {
|
||||||
if (this.oUrl) {
|
if (this.previewObjectUrl) {
|
||||||
URL.revokeObjectURL(this.oUrl);
|
URL.revokeObjectURL(this.previewObjectUrl);
|
||||||
this.oUrl = null;
|
this.previewObjectUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.thumb.remove();
|
this.thumb.remove();
|
||||||
this.$('.avatar').show();
|
this.$('.avatar').show();
|
||||||
this.$el.trigger('force-resize');
|
this.$el.trigger('force-resize');
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe("ContactBuffer", function() {
|
|||||||
var contactInfo = new textsecure.protobuf.ContactDetails({
|
var contactInfo = new textsecure.protobuf.ContactDetails({
|
||||||
name: "Zero Cool",
|
name: "Zero Cool",
|
||||||
number: "+10000000000",
|
number: "+10000000000",
|
||||||
avatar: { contentType: "image/jpg", length: avatarLen }
|
avatar: { contentType: "image/jpeg", length: avatarLen }
|
||||||
});
|
});
|
||||||
var contactInfoBuffer = contactInfo.encode().toArrayBuffer();
|
var contactInfoBuffer = contactInfo.encode().toArrayBuffer();
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ describe("ContactBuffer", function() {
|
|||||||
count++;
|
count++;
|
||||||
assert.strictEqual(contact.name, "Zero Cool");
|
assert.strictEqual(contact.name, "Zero Cool");
|
||||||
assert.strictEqual(contact.number, "+10000000000");
|
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.length, 255);
|
||||||
assert.strictEqual(contact.avatar.data.byteLength, 255);
|
assert.strictEqual(contact.avatar.data.byteLength, 255);
|
||||||
var avatarBytes = new Uint8Array(contact.avatar.data);
|
var avatarBytes = new Uint8Array(contact.avatar.data);
|
||||||
@@ -68,7 +68,7 @@ describe("GroupBuffer", function() {
|
|||||||
id: new Uint8Array([1, 3, 3, 7]).buffer,
|
id: new Uint8Array([1, 3, 3, 7]).buffer,
|
||||||
name: "Hackers",
|
name: "Hackers",
|
||||||
members: ['cereal', 'burn', 'phreak', 'joey'],
|
members: ['cereal', 'burn', 'phreak', 'joey'],
|
||||||
avatar: { contentType: "image/jpg", length: avatarLen }
|
avatar: { contentType: "image/jpeg", length: avatarLen }
|
||||||
});
|
});
|
||||||
var groupInfoBuffer = groupInfo.encode().toArrayBuffer();
|
var groupInfoBuffer = groupInfo.encode().toArrayBuffer();
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ describe("GroupBuffer", function() {
|
|||||||
assert.strictEqual(group.name, "Hackers");
|
assert.strictEqual(group.name, "Hackers");
|
||||||
assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer);
|
assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer);
|
||||||
assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']);
|
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.length, 255);
|
||||||
assert.strictEqual(group.avatar.data.byteLength, 255);
|
assert.strictEqual(group.avatar.data.byteLength, 255);
|
||||||
var avatarBytes = new Uint8Array(group.avatar.data);
|
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 topClearOfUpperBound = window.y >= boundsY;
|
||||||
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
|
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
|
||||||
|
|
||||||
return rightSideClearOfLeftBound
|
return rightSideClearOfLeftBound &&
|
||||||
&& leftSideClearOfRightBound
|
leftSideClearOfRightBound &&
|
||||||
&& topClearOfUpperBound
|
topClearOfUpperBound &&
|
||||||
&& topClearOfLowerBound;
|
topClearOfLowerBound;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
@@ -277,8 +277,8 @@ function createWindow() {
|
|||||||
// Emitted when the window is about to be closed.
|
// Emitted when the window is about to be closed.
|
||||||
mainWindow.on('close', (e) => {
|
mainWindow.on('close', (e) => {
|
||||||
// If the application is terminating, just do the default
|
// If the application is terminating, just do the default
|
||||||
if (windowState.shouldQuit()
|
if (windowState.shouldQuit() ||
|
||||||
|| config.environment === 'test' || config.environment === 'test-lib') {
|
config.environment === 'test' || config.environment === 'test-lib') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,9 +422,9 @@ app.on('before-quit', () => {
|
|||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
// On OS X it is common for applications and their menu bar
|
// On OS X it is common for applications and their menu bar
|
||||||
// to stay active until the user quits explicitly with Cmd + Q
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
if (process.platform !== 'darwin'
|
if (process.platform !== 'darwin' ||
|
||||||
|| config.environment === 'test'
|
config.environment === 'test' ||
|
||||||
|| config.environment === 'test-lib') {
|
config.environment === 'test-lib') {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
|
"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",
|
"lint": "grunt jshint",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"asarl": "asar l release/mac/Signal.app/Contents/Resources/app.asar",
|
"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-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-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",
|
"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-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",
|
"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",
|
"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",
|
"release": "npm run release-mac && npm run release-win && npm run release-lin",
|
||||||
"test-server": "mocha --recursive test/server",
|
"test-server": "mocha --recursive test/server",
|
||||||
"test-server-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/server",
|
"test-server-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/server",
|
||||||
|
"test-modules": "mocha --recursive test/modules",
|
||||||
"eslint": "eslint .",
|
"eslint": "eslint .",
|
||||||
"open-coverage": "open coverage/lcov-report/index.html"
|
"open-coverage": "open coverage/lcov-report/index.html"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"blob-util": "^1.3.0",
|
||||||
|
"blueimp-canvas-to-blob": "^3.14.0",
|
||||||
|
"blueimp-load-image": "^2.18.0",
|
||||||
"bunyan": "^1.8.12",
|
"bunyan": "^1.8.12",
|
||||||
"config": "^1.28.1",
|
"config": "^1.28.1",
|
||||||
"electron-config": "^1.0.0",
|
"electron-config": "^1.0.0",
|
||||||
|
|||||||
12
preload.js
12
preload.js
@@ -60,6 +60,8 @@
|
|||||||
window.nodeSetImmediate(function() {});
|
window.nodeSetImmediate(function() {});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
||||||
|
window.loadImage = require('blueimp-load-image');
|
||||||
window.ProxyAgent = require('proxy-agent');
|
window.ProxyAgent = require('proxy-agent');
|
||||||
window.EmojiConvertor = require('emoji-js');
|
window.EmojiConvertor = require('emoji-js');
|
||||||
window.emojiData = require('emoji-datasource');
|
window.emojiData = require('emoji-datasource');
|
||||||
@@ -70,6 +72,16 @@
|
|||||||
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
||||||
window.nodeNotifier = require('node-notifier');
|
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
|
// We pull this in last, because the native module involved appears to be sensitive to
|
||||||
// /tmp mounted as noexec on Linux.
|
// /tmp mounted as noexec on Linux.
|
||||||
require('./js/spell_check');
|
require('./js/spell_check');
|
||||||
|
|||||||
30
test/modules/types/mime_test.js
Normal file
30
test/modules/types/mime_test.js
Normal file
@@ -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:
|
dependencies:
|
||||||
readable-stream "^2.0.5"
|
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@*:
|
block-stream@*:
|
||||||
version "0.0.9"
|
version "0.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
|
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"
|
version "3.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
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:
|
bmp-js@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.1.tgz#5ad0147099d13a9f38aa7b99af1d6e78666ed37f"
|
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"
|
version "3.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
|
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:
|
import-lazy@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
|
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"
|
prelude-ls "~1.1.2"
|
||||||
type-check "~0.3.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:
|
livereload-js@^2.2.0:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2"
|
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"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
|
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:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
|
|||||||
Reference in New Issue
Block a user