mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
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
824 lines
27 KiB
JavaScript
824 lines
27 KiB
JavaScript
/* 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);
|
|
};
|
|
|
|
window.wrapDeferred = function(deferred) {
|
|
return new Promise(function(resolve, reject) {
|
|
deferred.then(resolve, reject);
|
|
});
|
|
};
|
|
|
|
console.log('background page reloaded');
|
|
console.log('environment:', window.config.environment);
|
|
|
|
var initialLoadComplete = false;
|
|
window.owsDesktopApp = {};
|
|
|
|
var title = window.config.name;
|
|
if (window.config.environment !== 'production') {
|
|
title += ' - ' + window.config.environment;
|
|
}
|
|
if (window.config.appInstance) {
|
|
title += ' - ' + window.config.appInstance;
|
|
}
|
|
window.config.title = window.document.title = title;
|
|
|
|
// start a background worker for ecc
|
|
textsecure.startWorker('js/libsignal-protocol-worker.js');
|
|
Whisper.KeyChangeListener.init(textsecure.storage.protocol);
|
|
textsecure.storage.protocol.on('removePreKey', function() {
|
|
getAccountManager().refreshPreKeys();
|
|
});
|
|
|
|
var SERVER_URL = window.config.serverUrl;
|
|
var CDN_URL = window.config.cdnUrl;
|
|
var messageReceiver;
|
|
window.getSocketStatus = function() {
|
|
if (messageReceiver) {
|
|
return messageReceiver.getStatus();
|
|
} else {
|
|
return -1;
|
|
}
|
|
};
|
|
Whisper.events = _.clone(Backbone.Events);
|
|
var accountManager;
|
|
window.getAccountManager = function() {
|
|
if (!accountManager) {
|
|
var USERNAME = storage.get('number_id');
|
|
var PASSWORD = storage.get('password');
|
|
accountManager = new textsecure.AccountManager(
|
|
SERVER_URL, USERNAME, PASSWORD
|
|
);
|
|
accountManager.addEventListener('registration', function() {
|
|
if (!Whisper.Registration.everDone()) {
|
|
storage.put('safety-numbers-approval', false);
|
|
}
|
|
Whisper.Registration.markDone();
|
|
console.log('dispatching registration event');
|
|
Whisper.events.trigger('registration_done');
|
|
});
|
|
}
|
|
return accountManager;
|
|
};
|
|
|
|
storage.fetch();
|
|
|
|
// We need this 'first' check because we don't want to start the app up any other time
|
|
// than the first time. And storage.fetch() will cause onready() to fire.
|
|
var first = true;
|
|
storage.onready(function() {
|
|
if (!first) {
|
|
return;
|
|
}
|
|
first = false;
|
|
|
|
ConversationController.load().then(start, start);
|
|
});
|
|
|
|
Whisper.events.on('shutdown', function() {
|
|
if (messageReceiver) {
|
|
messageReceiver.close().then(function() {
|
|
Whisper.events.trigger('shutdown-complete');
|
|
});
|
|
} else {
|
|
Whisper.events.trigger('shutdown-complete');
|
|
}
|
|
});
|
|
|
|
function start() {
|
|
var currentVersion = window.config.version;
|
|
var lastVersion = storage.get('version');
|
|
var newVersion = !lastVersion || currentVersion !== lastVersion;
|
|
storage.put('version', currentVersion);
|
|
|
|
if (newVersion) {
|
|
console.log('New version detected:', currentVersion);
|
|
}
|
|
|
|
window.dispatchEvent(new Event('storage_ready'));
|
|
|
|
console.log('listening for registration events');
|
|
Whisper.events.on('registration_done', function() {
|
|
console.log('handling registration event');
|
|
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
|
connect(true);
|
|
});
|
|
|
|
var appView = window.owsDesktopApp.appView = new Whisper.AppView({el: $('body')});
|
|
|
|
Whisper.WallClockListener.init(Whisper.events);
|
|
Whisper.ExpiringMessagesListener.init(Whisper.events);
|
|
|
|
if (Whisper.Import.isIncomplete()) {
|
|
console.log('Import was interrupted, showing import error screen');
|
|
appView.openImporter();
|
|
} else if (Whisper.Registration.everDone()) {
|
|
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
|
connect();
|
|
appView.openInbox({
|
|
initialLoadComplete: initialLoadComplete
|
|
});
|
|
} else {
|
|
appView.openInstallChoice();
|
|
}
|
|
|
|
Whisper.events.on('showDebugLog', function() {
|
|
appView.openDebugLog();
|
|
});
|
|
Whisper.events.on('unauthorized', function() {
|
|
appView.inboxView.networkStatusView.update();
|
|
});
|
|
Whisper.events.on('reconnectTimer', function() {
|
|
appView.inboxView.networkStatusView.setSocketReconnectInterval(60000);
|
|
});
|
|
Whisper.events.on('contactsync', function() {
|
|
if (appView.installView) {
|
|
appView.openInbox();
|
|
}
|
|
});
|
|
Whisper.events.on('contactsync:begin', function() {
|
|
if (appView.installView && appView.installView.showSync) {
|
|
appView.installView.showSync();
|
|
}
|
|
});
|
|
|
|
Whisper.Notifications.on('click', function(conversation) {
|
|
showWindow();
|
|
if (conversation) {
|
|
appView.openConversation(conversation);
|
|
} else {
|
|
appView.openInbox({
|
|
initialLoadComplete: initialLoadComplete
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
window.getSyncRequest = function() {
|
|
return new textsecure.SyncRequest(textsecure.messaging, messageReceiver);
|
|
};
|
|
|
|
Whisper.events.on('start-shutdown', function() {
|
|
if (messageReceiver) {
|
|
messageReceiver.close().then(function() {
|
|
Whisper.events.trigger('shutdown-complete');
|
|
});
|
|
} else {
|
|
Whisper.events.trigger('shutdown-complete');
|
|
}
|
|
});
|
|
|
|
|
|
var disconnectTimer = null;
|
|
function onOffline() {
|
|
console.log('offline');
|
|
|
|
window.removeEventListener('offline', onOffline);
|
|
window.addEventListener('online', onOnline);
|
|
|
|
// We've received logs from Linux where we get an 'offline' event, then 30ms later
|
|
// we get an online event. This waits a bit after getting an 'offline' event
|
|
// before disconnecting the socket manually.
|
|
disconnectTimer = setTimeout(disconnect, 1000);
|
|
}
|
|
|
|
function onOnline() {
|
|
console.log('online');
|
|
|
|
window.removeEventListener('online', onOnline);
|
|
window.addEventListener('offline', onOffline);
|
|
|
|
if (disconnectTimer && isSocketOnline()) {
|
|
console.log('Already online. Had a blip in online/offline status.');
|
|
clearTimeout(disconnectTimer);
|
|
disconnectTimer = null;
|
|
return;
|
|
}
|
|
if (disconnectTimer) {
|
|
clearTimeout(disconnectTimer);
|
|
disconnectTimer = null;
|
|
}
|
|
|
|
connect();
|
|
}
|
|
|
|
function isSocketOnline() {
|
|
var socketStatus = window.getSocketStatus();
|
|
return socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN;
|
|
}
|
|
|
|
function disconnect() {
|
|
console.log('disconnect');
|
|
|
|
// Clear timer, since we're only called when the timer is expired
|
|
disconnectTimer = null;
|
|
|
|
if (messageReceiver) {
|
|
messageReceiver.close();
|
|
}
|
|
}
|
|
|
|
var connectCount = 0;
|
|
function connect(firstRun) {
|
|
console.log('connect');
|
|
|
|
// Bootstrap our online/offline detection, only the first time we connect
|
|
if (connectCount === 0 && navigator.onLine) {
|
|
window.addEventListener('offline', onOffline);
|
|
}
|
|
if (connectCount === 0 && !navigator.onLine) {
|
|
console.log('Starting up offline; will connect when we have network access');
|
|
window.addEventListener('online', onOnline);
|
|
onEmpty(); // this ensures that the loading screen is dismissed
|
|
return;
|
|
}
|
|
|
|
if (!Whisper.Registration.everDone()) { return; }
|
|
if (Whisper.Import.isIncomplete()) { return; }
|
|
|
|
if (messageReceiver) {
|
|
messageReceiver.close();
|
|
}
|
|
|
|
var USERNAME = storage.get('number_id');
|
|
var PASSWORD = storage.get('password');
|
|
var mySignalingKey = storage.get('signaling_key');
|
|
|
|
connectCount += 1;
|
|
var options = {
|
|
retryCached: connectCount === 1,
|
|
};
|
|
|
|
Whisper.Notifications.disable(); // avoid notification flood until empty
|
|
|
|
// initialize the socket and start listening for messages
|
|
messageReceiver = new textsecure.MessageReceiver(
|
|
SERVER_URL, USERNAME, PASSWORD, mySignalingKey, options
|
|
);
|
|
messageReceiver.addEventListener('message', onMessageReceived);
|
|
messageReceiver.addEventListener('delivery', onDeliveryReceipt);
|
|
messageReceiver.addEventListener('contact', onContactReceived);
|
|
messageReceiver.addEventListener('group', onGroupReceived);
|
|
messageReceiver.addEventListener('sent', onSentMessage);
|
|
messageReceiver.addEventListener('readSync', onReadSync);
|
|
messageReceiver.addEventListener('read', onReadReceipt);
|
|
messageReceiver.addEventListener('verified', onVerified);
|
|
messageReceiver.addEventListener('error', onError);
|
|
messageReceiver.addEventListener('empty', onEmpty);
|
|
messageReceiver.addEventListener('progress', onProgress);
|
|
messageReceiver.addEventListener('configuration', onConfiguration);
|
|
|
|
window.textsecure.messaging = new textsecure.MessageSender(
|
|
SERVER_URL, USERNAME, PASSWORD, CDN_URL
|
|
);
|
|
|
|
// Because v0.43.2 introduced a bug that lost contact details, v0.43.4 introduces
|
|
// a one-time contact sync to restore all lost contact/group information. We
|
|
// disable this checking if a user is first registering.
|
|
var key = 'chrome-contact-sync-v0.43.4';
|
|
if (!storage.get(key)) {
|
|
storage.put(key, true);
|
|
|
|
if (!firstRun && textsecure.storage.user.getDeviceId() != '1') {
|
|
window.getSyncRequest();
|
|
}
|
|
}
|
|
|
|
// If we've just upgraded to read receipt support on desktop, kick off a
|
|
// one-time configuration sync request to get the read-receipt setting
|
|
// from the master device.
|
|
var readReceiptConfigurationSync = 'read-receipt-configuration-sync';
|
|
if (!storage.get(readReceiptConfigurationSync)) {
|
|
|
|
if (!firstRun && textsecure.storage.user.getDeviceId() != '1') {
|
|
textsecure.messaging.sendRequestConfigurationSyncMessage().then(function() {
|
|
storage.put(readReceiptConfigurationSync, true);
|
|
}).catch(function(e) {
|
|
console.log(e);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (firstRun === true && textsecure.storage.user.getDeviceId() != '1') {
|
|
if (!storage.get('theme-setting') && textsecure.storage.get('userAgent') === 'OWI') {
|
|
storage.put('theme-setting', 'ios');
|
|
}
|
|
var syncRequest = new textsecure.SyncRequest(textsecure.messaging, messageReceiver);
|
|
Whisper.events.trigger('contactsync:begin');
|
|
syncRequest.addEventListener('success', function() {
|
|
console.log('sync successful');
|
|
storage.put('synced_at', Date.now());
|
|
Whisper.events.trigger('contactsync');
|
|
});
|
|
syncRequest.addEventListener('timeout', function() {
|
|
console.log('sync timed out');
|
|
Whisper.events.trigger('contactsync');
|
|
});
|
|
|
|
if (Whisper.Import.isComplete()) {
|
|
textsecure.messaging.sendRequestConfigurationSyncMessage().catch(function(e) {
|
|
console.log(e);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function onEmpty() {
|
|
initialLoadComplete = true;
|
|
|
|
var interval = setInterval(function() {
|
|
var view = window.owsDesktopApp.appView;
|
|
if (view) {
|
|
clearInterval(interval);
|
|
interval = null;
|
|
view.onEmpty();
|
|
}
|
|
}, 500);
|
|
|
|
Whisper.Notifications.enable();
|
|
}
|
|
function onProgress(ev) {
|
|
var count = ev.count;
|
|
|
|
var view = window.owsDesktopApp.appView;
|
|
if (view) {
|
|
view.onProgress(count);
|
|
}
|
|
}
|
|
function onConfiguration(ev) {
|
|
storage.put('read-receipt-setting', ev.configuration.readReceipts);
|
|
}
|
|
|
|
function onContactReceived(ev) {
|
|
var details = ev.contactDetails;
|
|
|
|
var id = details.number;
|
|
|
|
if (id === textsecure.storage.user.getNumber()) {
|
|
// special case for syncing details about ourselves
|
|
if (details.profileKey) {
|
|
console.log('Got sync message with our own profile key');
|
|
storage.put('profileKey', details.profileKey);
|
|
}
|
|
}
|
|
|
|
var c = new Whisper.Conversation({
|
|
id: id
|
|
});
|
|
var error = c.validateNumber();
|
|
if (error) {
|
|
console.log('Invalid contact received', error && error.stack ? error.stack : error);
|
|
return;
|
|
}
|
|
|
|
return ConversationController.getOrCreateAndWait(id, 'private')
|
|
.then(function(conversation) {
|
|
var activeAt = conversation.get('active_at');
|
|
|
|
// The idea is to make any new contact show up in the left pane. If
|
|
// activeAt is null, then this contact has been purposefully hidden.
|
|
if (activeAt !== null) {
|
|
activeAt = activeAt || Date.now();
|
|
}
|
|
|
|
if (details.profileKey) {
|
|
conversation.set({profileKey: details.profileKey});
|
|
}
|
|
|
|
if (typeof details.blocked !== 'undefined') {
|
|
if (details.blocked) {
|
|
storage.addBlockedNumber(id);
|
|
} else {
|
|
storage.removeBlockedNumber(id);
|
|
}
|
|
}
|
|
|
|
return wrapDeferred(conversation.save({
|
|
name: details.name,
|
|
avatar: details.avatar,
|
|
color: details.color,
|
|
active_at: activeAt,
|
|
})).then(function() {
|
|
// this needs to be inline to get access to conversation model
|
|
if (typeof details.expireTimer !== 'undefined') {
|
|
var source = textsecure.storage.user.getNumber();
|
|
var receivedAt = Date.now();
|
|
return conversation.updateExpirationTimer(
|
|
details.expireTimer,
|
|
source,
|
|
receivedAt,
|
|
{fromSync: true}
|
|
);
|
|
}
|
|
});
|
|
})
|
|
.then(function() {
|
|
if (details.verified) {
|
|
var verified = details.verified;
|
|
var ev = new Event('verified');
|
|
ev.verified = {
|
|
state: verified.state,
|
|
destination: verified.destination,
|
|
identityKey: verified.identityKey.toArrayBuffer(),
|
|
};
|
|
ev.viaContactSync = true;
|
|
return onVerified(ev);
|
|
}
|
|
})
|
|
.then(ev.confirm)
|
|
.catch(function(error) {
|
|
console.log(
|
|
'onContactReceived error:',
|
|
error && error.stack ? error.stack : error
|
|
);
|
|
});
|
|
}
|
|
|
|
function onGroupReceived(ev) {
|
|
var details = ev.groupDetails;
|
|
var id = details.id;
|
|
|
|
return ConversationController.getOrCreateAndWait(id, 'group').then(function(conversation) {
|
|
var updates = {
|
|
name: details.name,
|
|
members: details.members,
|
|
avatar: details.avatar,
|
|
type: 'group',
|
|
};
|
|
if (details.active) {
|
|
var activeAt = conversation.get('active_at');
|
|
|
|
// The idea is to make any new group show up in the left pane. If
|
|
// activeAt is null, then this group has been purposefully hidden.
|
|
if (activeAt !== null) {
|
|
updates.active_at = activeAt || Date.now();
|
|
}
|
|
updates.left = false;
|
|
} else {
|
|
updates.left = true;
|
|
}
|
|
|
|
return wrapDeferred(conversation.save(updates)).then(function() {
|
|
if (typeof details.expireTimer !== 'undefined') {
|
|
var source = textsecure.storage.user.getNumber();
|
|
var receivedAt = Date.now();
|
|
return conversation.updateExpirationTimer(
|
|
details.expireTimer,
|
|
source,
|
|
receivedAt,
|
|
{fromSync: true}
|
|
);
|
|
}
|
|
}).then(ev.confirm);
|
|
});
|
|
}
|
|
|
|
/* eslint-enable */
|
|
/* jshint ignore:start */
|
|
|
|
// Descriptors
|
|
const getGroupDescriptor = group => ({
|
|
type: Message.GROUP,
|
|
id: group.id,
|
|
});
|
|
|
|
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
|
const getDescriptorForSent = ({ message, destination }) => (
|
|
message.group
|
|
? getGroupDescriptor(message.group)
|
|
: { type: Message.PRIVATE, id: destination }
|
|
);
|
|
|
|
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
|
const getDescriptorForReceived = ({ message, source }) => (
|
|
message.group
|
|
? getGroupDescriptor(message.group)
|
|
: { type: Message.PRIVATE, id: source }
|
|
);
|
|
|
|
function createMessageHandler({
|
|
createMessage,
|
|
getMessageDescriptor,
|
|
handleProfileUpdate,
|
|
}) {
|
|
return async (event) => {
|
|
const { data, confirm } = event;
|
|
|
|
const messageDescriptor = getMessageDescriptor(data);
|
|
|
|
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 });
|
|
}
|
|
|
|
const message = createMessage(data);
|
|
const isDuplicate = await isMessageDuplicate(message);
|
|
if (isDuplicate) {
|
|
console.log('Received duplicate message', message.idForLogging());
|
|
return event.confirm();
|
|
}
|
|
|
|
const upgradedMessage = await Message.upgradeSchema(data.message);
|
|
await ConversationController.getOrCreateAndWait(
|
|
messageDescriptor.id,
|
|
messageDescriptor.type
|
|
);
|
|
return message.handleDataMessage(
|
|
upgradedMessage,
|
|
event.confirm,
|
|
{ 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) {
|
|
var fetcher = new Whisper.Message();
|
|
var options = {
|
|
index: {
|
|
name: 'unique',
|
|
value: [
|
|
message.get('source'),
|
|
message.get('sourceDevice'),
|
|
message.get('sent_at')
|
|
]
|
|
}
|
|
};
|
|
|
|
fetcher.fetch(options).always(function() {
|
|
if (fetcher.get('id')) {
|
|
return resolve(true);
|
|
}
|
|
|
|
return resolve(false);
|
|
});
|
|
}).catch(function(error) {
|
|
console.log('isMessageDuplicate error:', error && error.stack ? error.stack : error);
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function initIncomingMessage(data) {
|
|
var message = new Whisper.Message({
|
|
source : data.source,
|
|
sourceDevice : data.sourceDevice,
|
|
sent_at : data.timestamp,
|
|
received_at : data.receivedAt || Date.now(),
|
|
conversationId : data.source,
|
|
type : 'incoming',
|
|
unread : 1
|
|
});
|
|
|
|
return message;
|
|
}
|
|
|
|
function onError(ev) {
|
|
var error = ev.error;
|
|
console.log('background onError:', error && error.stack ? error.stack : error);
|
|
|
|
if (error.name === 'HTTPError' && (error.code == 401 || error.code == 403)) {
|
|
Whisper.Registration.remove();
|
|
Whisper.events.trigger('unauthorized');
|
|
return;
|
|
}
|
|
|
|
if (error.name === 'HTTPError' && error.code == -1) {
|
|
// Failed to connect to server
|
|
if (navigator.onLine) {
|
|
console.log('retrying in 1 minute');
|
|
setTimeout(connect, 60000);
|
|
|
|
Whisper.events.trigger('reconnectTimer');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (ev.proto) {
|
|
if (error.name === 'MessageCounterError') {
|
|
if (ev.confirm) {
|
|
ev.confirm();
|
|
}
|
|
// Ignore this message. It is likely a duplicate delivery
|
|
// because the server lost our ack the first time.
|
|
return;
|
|
}
|
|
var envelope = ev.proto;
|
|
var message = initIncomingMessage(envelope);
|
|
|
|
return message.saveErrors(error).then(function() {
|
|
var id = message.get('conversationId');
|
|
return ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) {
|
|
conversation.set({
|
|
active_at: Date.now(),
|
|
unreadCount: conversation.get('unreadCount') + 1
|
|
});
|
|
|
|
var conversation_timestamp = conversation.get('timestamp');
|
|
var message_timestamp = message.get('timestamp');
|
|
if (!conversation_timestamp || message_timestamp > conversation_timestamp) {
|
|
conversation.set({ timestamp: message.get('sent_at') });
|
|
}
|
|
|
|
conversation.trigger('newmessage', message);
|
|
conversation.notify(message);
|
|
|
|
if (ev.confirm) {
|
|
ev.confirm();
|
|
}
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
conversation.save().then(resolve, reject);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
function onReadReceipt(ev) {
|
|
var read_at = ev.timestamp;
|
|
var timestamp = ev.read.timestamp;
|
|
var reader = ev.read.reader;
|
|
console.log('read receipt', reader, timestamp);
|
|
|
|
if (!storage.get('read-receipt-setting')) {
|
|
return ev.confirm();
|
|
}
|
|
|
|
var receipt = Whisper.ReadReceipts.add({
|
|
reader : reader,
|
|
timestamp : timestamp,
|
|
read_at : read_at,
|
|
});
|
|
|
|
receipt.on('remove', ev.confirm);
|
|
|
|
// Calling this directly so we can wait for completion
|
|
return Whisper.ReadReceipts.onReceipt(receipt);
|
|
}
|
|
|
|
function onReadSync(ev) {
|
|
var read_at = ev.timestamp;
|
|
var timestamp = ev.read.timestamp;
|
|
var sender = ev.read.sender;
|
|
console.log('read sync', sender, timestamp);
|
|
|
|
var receipt = Whisper.ReadSyncs.add({
|
|
sender : sender,
|
|
timestamp : timestamp,
|
|
read_at : read_at
|
|
});
|
|
|
|
receipt.on('remove', ev.confirm);
|
|
|
|
// Calling this directly so we can wait for completion
|
|
return Whisper.ReadSyncs.onReceipt(receipt);
|
|
}
|
|
|
|
function onVerified(ev) {
|
|
var number = ev.verified.destination;
|
|
var key = ev.verified.identityKey;
|
|
var state;
|
|
|
|
var c = new Whisper.Conversation({
|
|
id: number
|
|
});
|
|
var error = c.validateNumber();
|
|
if (error) {
|
|
console.log(
|
|
'Invalid verified sync received',
|
|
error && error.stack ? error.stack : error
|
|
);
|
|
return;
|
|
}
|
|
|
|
switch(ev.verified.state) {
|
|
case textsecure.protobuf.Verified.State.DEFAULT:
|
|
state = 'DEFAULT';
|
|
break;
|
|
case textsecure.protobuf.Verified.State.VERIFIED:
|
|
state = 'VERIFIED';
|
|
break;
|
|
case textsecure.protobuf.Verified.State.UNVERIFIED:
|
|
state = 'UNVERIFIED';
|
|
break;
|
|
}
|
|
|
|
console.log('got verified sync for', number, state,
|
|
ev.viaContactSync ? 'via contact sync' : '');
|
|
|
|
return ConversationController.getOrCreateAndWait(number, 'private').then(function(contact) {
|
|
var options = {
|
|
viaSyncMessage: true,
|
|
viaContactSync: ev.viaContactSync,
|
|
key: key
|
|
};
|
|
|
|
if (state === 'VERIFIED') {
|
|
return contact.setVerified(options).then(ev.confirm);
|
|
} else if (state === 'DEFAULT') {
|
|
return contact.setVerifiedDefault(options).then(ev.confirm);
|
|
} else {
|
|
return contact.setUnverified(options).then(ev.confirm);
|
|
}
|
|
});
|
|
}
|
|
|
|
function onDeliveryReceipt(ev) {
|
|
var deliveryReceipt = ev.deliveryReceipt;
|
|
console.log(
|
|
'delivery receipt from',
|
|
deliveryReceipt.source + '.' + deliveryReceipt.sourceDevice,
|
|
deliveryReceipt.timestamp
|
|
);
|
|
|
|
var receipt = Whisper.DeliveryReceipts.add({
|
|
timestamp: deliveryReceipt.timestamp,
|
|
source: deliveryReceipt.source
|
|
});
|
|
|
|
ev.confirm();
|
|
|
|
// Calling this directly so we can wait for completion
|
|
return Whisper.DeliveryReceipts.onReceipt(receipt);
|
|
}
|
|
})();
|