mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 10:19:08 +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
491 lines
13 KiB
JavaScript
491 lines
13 KiB
JavaScript
const path = require('path');
|
|
const url = require('url');
|
|
const os = require('os');
|
|
|
|
const _ = require('lodash');
|
|
const electron = require('electron');
|
|
const semver = require('semver');
|
|
|
|
const {
|
|
BrowserWindow,
|
|
app,
|
|
Menu,
|
|
shell,
|
|
ipcMain: ipc,
|
|
} = electron;
|
|
|
|
const packageJson = require('./package.json');
|
|
|
|
const createTrayIcon = require('./app/tray_icon');
|
|
const createTemplate = require('./app/menu.js');
|
|
const logging = require('./app/logging');
|
|
const autoUpdate = require('./app/auto_update');
|
|
const windowState = require('./app/window_state');
|
|
|
|
|
|
const aumid = `org.whispersystems.${packageJson.name}`;
|
|
console.log(`setting AUMID to ${aumid}`);
|
|
app.setAppUserModelId(aumid);
|
|
|
|
// Keep a global reference of the window object, if you don't, the window will
|
|
// be closed automatically when the JavaScript object is garbage collected.
|
|
let mainWindow;
|
|
|
|
function getMainWindow() {
|
|
return mainWindow;
|
|
}
|
|
|
|
// Tray icon and related objects
|
|
let tray = null;
|
|
const startInTray = process.argv.find(arg => arg === '--start-in-tray');
|
|
const usingTrayIcon = startInTray || process.argv.find(arg => arg === '--use-tray-icon');
|
|
|
|
const config = require('./app/config');
|
|
|
|
// Very important to put before the single instance check, since it is based on the
|
|
// userData directory.
|
|
const userConfig = require('./app/user_config');
|
|
|
|
function showWindow() {
|
|
if (!mainWindow) {
|
|
return;
|
|
}
|
|
|
|
// Using focus() instead of show() seems to be important on Windows when our window
|
|
// has been docked using Aero Snap/Snap Assist. A full .show() call here will cause
|
|
// the window to reposition:
|
|
// https://github.com/signalapp/Signal-Desktop/issues/1429
|
|
if (mainWindow.isVisible()) {
|
|
mainWindow.focus();
|
|
} else {
|
|
mainWindow.show();
|
|
}
|
|
|
|
// toggle the visibility of the show/hide tray icon menu entries
|
|
if (tray) {
|
|
tray.updateContextMenu();
|
|
}
|
|
}
|
|
|
|
if (!process.mas) {
|
|
console.log('making app single instance');
|
|
const shouldQuit = app.makeSingleInstance(() => {
|
|
// Someone tried to run a second instance, we should focus our window
|
|
if (mainWindow) {
|
|
if (mainWindow.isMinimized()) {
|
|
mainWindow.restore();
|
|
}
|
|
|
|
showWindow();
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (shouldQuit) {
|
|
console.log('quitting; we are the second instance');
|
|
app.exit();
|
|
}
|
|
}
|
|
|
|
let windowConfig = userConfig.get('window');
|
|
const loadLocale = require('./app/locale').load;
|
|
|
|
// Both of these will be set after app fires the 'ready' event
|
|
let logger;
|
|
let locale;
|
|
|
|
const WINDOWS_8 = '8.0.0';
|
|
const osRelease = os.release();
|
|
const polyfillNotifications =
|
|
os.platform() === 'win32' && semver.lt(osRelease, WINDOWS_8);
|
|
console.log('OS Release:', osRelease, '- notifications polyfill?', polyfillNotifications);
|
|
|
|
function prepareURL(pathSegments) {
|
|
return url.format({
|
|
pathname: path.join.apply(null, pathSegments),
|
|
protocol: 'file:',
|
|
slashes: true,
|
|
query: {
|
|
name: packageJson.productName,
|
|
locale: locale.name,
|
|
version: app.getVersion(),
|
|
buildExpiration: config.get('buildExpiration'),
|
|
serverUrl: config.get('serverUrl'),
|
|
cdnUrl: config.get('cdnUrl'),
|
|
certificateAuthorities: config.get('certificateAuthorities'),
|
|
environment: config.environment,
|
|
node_version: process.versions.node,
|
|
hostname: os.hostname(),
|
|
appInstance: process.env.NODE_APP_INSTANCE,
|
|
polyfillNotifications: polyfillNotifications ? true : undefined, // for stringify()
|
|
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy,
|
|
},
|
|
});
|
|
}
|
|
|
|
function handleUrl(event, target) {
|
|
event.preventDefault();
|
|
const { protocol } = url.parse(target);
|
|
if (protocol === 'http:' || protocol === 'https:') {
|
|
shell.openExternal(target);
|
|
}
|
|
}
|
|
|
|
function captureClicks(window) {
|
|
window.webContents.on('will-navigate', handleUrl);
|
|
window.webContents.on('new-window', handleUrl);
|
|
}
|
|
|
|
|
|
const DEFAULT_WIDTH = 800;
|
|
const DEFAULT_HEIGHT = 610;
|
|
const MIN_WIDTH = 640;
|
|
const MIN_HEIGHT = 360;
|
|
const BOUNDS_BUFFER = 100;
|
|
|
|
function isVisible(window, bounds) {
|
|
const boundsX = _.get(bounds, 'x') || 0;
|
|
const boundsY = _.get(bounds, 'y') || 0;
|
|
const boundsWidth = _.get(bounds, 'width') || DEFAULT_WIDTH;
|
|
const boundsHeight = _.get(bounds, 'height') || DEFAULT_HEIGHT;
|
|
|
|
// requiring BOUNDS_BUFFER pixels on the left or right side
|
|
const rightSideClearOfLeftBound = (window.x + window.width >= boundsX + BOUNDS_BUFFER);
|
|
const leftSideClearOfRightBound = (window.x <= (boundsX + boundsWidth) - BOUNDS_BUFFER);
|
|
|
|
// top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom
|
|
const topClearOfUpperBound = window.y >= boundsY;
|
|
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
|
|
|
|
return rightSideClearOfLeftBound &&
|
|
leftSideClearOfRightBound &&
|
|
topClearOfUpperBound &&
|
|
topClearOfLowerBound;
|
|
}
|
|
|
|
function createWindow() {
|
|
const { screen } = electron;
|
|
const windowOptions = Object.assign({
|
|
show: !startInTray, // allow to start minimised in tray
|
|
width: DEFAULT_WIDTH,
|
|
height: DEFAULT_HEIGHT,
|
|
minWidth: MIN_WIDTH,
|
|
minHeight: MIN_HEIGHT,
|
|
autoHideMenuBar: false,
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
// sandbox: true,
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
},
|
|
icon: path.join(__dirname, 'images', 'icon_256.png'),
|
|
}, _.pick(windowConfig, ['maximized', 'autoHideMenuBar', 'width', 'height', 'x', 'y']));
|
|
|
|
if (!_.isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) {
|
|
windowOptions.width = DEFAULT_WIDTH;
|
|
}
|
|
if (!_.isNumber(windowOptions.height) || windowOptions.height < MIN_HEIGHT) {
|
|
windowOptions.height = DEFAULT_HEIGHT;
|
|
}
|
|
if (!_.isBoolean(windowOptions.maximized)) {
|
|
delete windowOptions.maximized;
|
|
}
|
|
if (!_.isBoolean(windowOptions.autoHideMenuBar)) {
|
|
delete windowOptions.autoHideMenuBar;
|
|
}
|
|
|
|
const visibleOnAnyScreen = _.some(screen.getAllDisplays(), (display) => {
|
|
if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) {
|
|
return false;
|
|
}
|
|
|
|
return isVisible(windowOptions, _.get(display, 'bounds'));
|
|
});
|
|
if (!visibleOnAnyScreen) {
|
|
console.log('Location reset needed');
|
|
delete windowOptions.x;
|
|
delete windowOptions.y;
|
|
}
|
|
|
|
if (windowOptions.fullscreen === false) {
|
|
delete windowOptions.fullscreen;
|
|
}
|
|
|
|
logger.info('Initializing BrowserWindow config: %s', JSON.stringify(windowOptions));
|
|
|
|
// Create the browser window.
|
|
mainWindow = new BrowserWindow(windowOptions);
|
|
|
|
function captureAndSaveWindowStats() {
|
|
const size = mainWindow.getSize();
|
|
const position = mainWindow.getPosition();
|
|
|
|
// so if we need to recreate the window, we have the most recent settings
|
|
windowConfig = {
|
|
maximized: mainWindow.isMaximized(),
|
|
autoHideMenuBar: mainWindow.isMenuBarAutoHide(),
|
|
width: size[0],
|
|
height: size[1],
|
|
x: position[0],
|
|
y: position[1],
|
|
};
|
|
|
|
if (mainWindow.isFullScreen()) {
|
|
// Only include this property if true, because when explicitly set to
|
|
// false the fullscreen button will be disabled on osx
|
|
windowConfig.fullscreen = true;
|
|
}
|
|
|
|
logger.info('Updating BrowserWindow config: %s', JSON.stringify(windowConfig));
|
|
userConfig.set('window', windowConfig);
|
|
}
|
|
|
|
const debouncedCaptureStats = _.debounce(captureAndSaveWindowStats, 500);
|
|
mainWindow.on('resize', debouncedCaptureStats);
|
|
mainWindow.on('move', debouncedCaptureStats);
|
|
mainWindow.on('close', captureAndSaveWindowStats);
|
|
|
|
mainWindow.on('focus', () => {
|
|
mainWindow.flashFrame(false);
|
|
});
|
|
|
|
// Ingested in preload.js via a sendSync call
|
|
ipc.on('locale-data', (event) => {
|
|
// eslint-disable-next-line no-param-reassign
|
|
event.returnValue = locale.messages;
|
|
});
|
|
|
|
if (config.environment === 'test') {
|
|
mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html']));
|
|
} else if (config.environment === 'test-lib') {
|
|
mainWindow.loadURL(prepareURL([__dirname, 'libtextsecure', 'test', 'index.html']));
|
|
} else {
|
|
mainWindow.loadURL(prepareURL([__dirname, 'background.html']));
|
|
}
|
|
|
|
if (config.get('openDevTools')) {
|
|
// Open the DevTools.
|
|
mainWindow.webContents.openDevTools();
|
|
}
|
|
|
|
captureClicks(mainWindow);
|
|
|
|
mainWindow.webContents.on('will-navigate', (e) => {
|
|
logger.info('will-navigate');
|
|
e.preventDefault();
|
|
});
|
|
|
|
// Emitted when the window is about to be closed.
|
|
mainWindow.on('close', (e) => {
|
|
// If the application is terminating, just do the default
|
|
if (windowState.shouldQuit() ||
|
|
config.environment === 'test' || config.environment === 'test-lib') {
|
|
return;
|
|
}
|
|
|
|
// On Mac, or on other platforms when the tray icon is in use, the window
|
|
// should be only hidden, not closed, when the user clicks the close button
|
|
if (usingTrayIcon || process.platform === 'darwin') {
|
|
e.preventDefault();
|
|
mainWindow.hide();
|
|
|
|
// toggle the visibility of the show/hide tray icon menu entries
|
|
if (tray) {
|
|
tray.updateContextMenu();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Emitted when the window is closed.
|
|
mainWindow.on('closed', () => {
|
|
// Dereference the window object, usually you would store windows
|
|
// in an array if your app supports multi windows, this is the time
|
|
// when you should delete the corresponding element.
|
|
mainWindow = null;
|
|
});
|
|
|
|
ipc.on('show-window', () => {
|
|
showWindow();
|
|
});
|
|
}
|
|
|
|
function showDebugLog() {
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send('debug-log');
|
|
}
|
|
}
|
|
|
|
function openReleaseNotes() {
|
|
shell.openExternal(`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`);
|
|
}
|
|
|
|
function openNewBugForm() {
|
|
shell.openExternal('https://github.com/signalapp/Signal-Desktop/issues/new');
|
|
}
|
|
|
|
function openSupportPage() {
|
|
shell.openExternal('https://support.signal.org/hc/en-us/categories/202319038-Desktop');
|
|
}
|
|
|
|
function openForums() {
|
|
shell.openExternal('https://whispersystems.discoursehosting.net/');
|
|
}
|
|
|
|
|
|
let aboutWindow;
|
|
function showAbout() {
|
|
if (aboutWindow) {
|
|
aboutWindow.show();
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
width: 500,
|
|
height: 400,
|
|
resizable: false,
|
|
title: locale.messages.aboutSignalDesktop.message,
|
|
autoHideMenuBar: true,
|
|
backgroundColor: '#2090EA',
|
|
show: false,
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
},
|
|
parent: mainWindow,
|
|
};
|
|
|
|
aboutWindow = new BrowserWindow(options);
|
|
|
|
captureClicks(aboutWindow);
|
|
|
|
aboutWindow.loadURL(prepareURL([__dirname, 'about.html']));
|
|
|
|
aboutWindow.on('closed', () => {
|
|
aboutWindow = null;
|
|
});
|
|
|
|
aboutWindow.once('ready-to-show', () => {
|
|
aboutWindow.show();
|
|
});
|
|
}
|
|
|
|
// This method will be called when Electron has finished
|
|
// initialization and is ready to create browser windows.
|
|
// Some APIs can only be used after this event occurs.
|
|
let ready = false;
|
|
app.on('ready', () => {
|
|
let loggingSetupError;
|
|
logging.initialize().catch((error) => {
|
|
loggingSetupError = error;
|
|
}).then(() => {
|
|
logger = logging.getLogger();
|
|
logger.info('app ready');
|
|
|
|
if (loggingSetupError) {
|
|
logger.error('Problem setting up logging', loggingSetupError.stack);
|
|
}
|
|
|
|
if (!locale) {
|
|
locale = loadLocale();
|
|
}
|
|
|
|
ready = true;
|
|
|
|
autoUpdate.initialize(getMainWindow, locale.messages);
|
|
|
|
createWindow();
|
|
|
|
if (usingTrayIcon) {
|
|
tray = createTrayIcon(getMainWindow, locale.messages);
|
|
}
|
|
|
|
const options = {
|
|
showDebugLog,
|
|
showWindow,
|
|
showAbout,
|
|
openReleaseNotes,
|
|
openNewBugForm,
|
|
openSupportPage,
|
|
openForums,
|
|
};
|
|
const template = createTemplate(options, locale.messages);
|
|
|
|
const menu = Menu.buildFromTemplate(template);
|
|
Menu.setApplicationMenu(menu);
|
|
});
|
|
});
|
|
|
|
app.on('before-quit', () => {
|
|
windowState.markShouldQuit();
|
|
});
|
|
|
|
// Quit when all windows are closed.
|
|
app.on('window-all-closed', () => {
|
|
// On OS X it is common for applications and their menu bar
|
|
// to stay active until the user quits explicitly with Cmd + Q
|
|
if (process.platform !== 'darwin' ||
|
|
config.environment === 'test' ||
|
|
config.environment === 'test-lib') {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
if (!ready) {
|
|
return;
|
|
}
|
|
|
|
// On OS X it's common to re-create a window in the app when the
|
|
// dock icon is clicked and there are no other windows open.
|
|
if (mainWindow) {
|
|
mainWindow.show();
|
|
} else {
|
|
createWindow();
|
|
}
|
|
});
|
|
|
|
ipc.on('set-badge-count', (event, count) => {
|
|
app.setBadgeCount(count);
|
|
});
|
|
|
|
ipc.on('draw-attention', () => {
|
|
if (process.platform === 'darwin') {
|
|
app.dock.bounce();
|
|
} else if (process.platform === 'win32') {
|
|
mainWindow.flashFrame(true);
|
|
setTimeout(() => {
|
|
mainWindow.flashFrame(false);
|
|
}, 1000);
|
|
} else if (process.platform === 'linux') {
|
|
mainWindow.flashFrame(true);
|
|
}
|
|
});
|
|
|
|
ipc.on('restart', () => {
|
|
app.relaunch();
|
|
app.quit();
|
|
});
|
|
|
|
ipc.on('set-auto-hide-menu-bar', (event, autoHide) => {
|
|
if (mainWindow) {
|
|
mainWindow.setAutoHideMenuBar(autoHide);
|
|
}
|
|
});
|
|
|
|
ipc.on('set-menu-bar-visibility', (event, visibility) => {
|
|
if (mainWindow) {
|
|
mainWindow.setMenuBarVisibility(visibility);
|
|
}
|
|
});
|
|
|
|
ipc.on('close-about', () => {
|
|
if (aboutWindow) {
|
|
aboutWindow.close();
|
|
}
|
|
});
|
|
|
|
ipc.on('update-tray-icon', (event, unreadCount) => {
|
|
if (tray) {
|
|
tray.updateIcon(unreadCount);
|
|
}
|
|
});
|