Promote 1.5.0-beta.1 (and a couple bugfixes) to production

This commit is contained in:
Scott Nonnenberg 2018-02-26 15:54:25 -08:00
commit c0bee47f92
No known key found for this signature in database
GPG key ID: A4931C09644C654B
72 changed files with 2733 additions and 1436 deletions

14
.editorconfig Normal file
View 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

View file

@ -1,17 +1,23 @@
build/**
components/**
coverage/**
dist/**
libtextsecure/**
coverage/**
# these aren't ready yet, pulling files in one-by-one
js/**
test/**
js/*.js
js/models/**/*.js
js/react/**/*.js
js/views/**/*.js
test/*.js
test/models/*.js
test/views/*.js
/*.js
# ES2015+ files
!js/background.js
!js/models/conversations.js
!js/views/file_input_view.js
!js/views/attachment_view.js
!main.js
!prepare_build.js
# all of these files will be new
!test/server/**/*.js
# all of app/ is included

View file

@ -11,6 +11,10 @@ module.exports = {
'airbnb-base',
],
plugins: [
'more',
],
rules: {
'comma-dangle': ['error', {
arrays: 'always-multiline',
@ -21,7 +25,7 @@ module.exports = {
}],
// putting params on their own line helps stay within line length limit
'function-paren-newline': ['error', 'consistent'],
'function-paren-newline': ['error', 'multiline'],
// 90 characters allows three+ side-by-side screens on a standard-size monitor
'max-len': ['error', {
@ -29,6 +33,9 @@ module.exports = {
ignoreUrls: true,
}],
// encourage consistent use of `async` / `await` instead of `then`
'more/no-then': 'error',
// it helps readability to put public API at top,
'no-use-before-define': 'off',
@ -37,5 +44,8 @@ module.exports = {
// though we have a logger, we still remap console to log to disk
'no-console': 'off',
// consistently place operators at end of line except ternaries
'operator-linebreak': 'error',
}
};

View file

@ -59,7 +59,7 @@ Operating System:
<!-- Instructions for finding your OS version are here: http://whatsmyos.com/ -->
Linked device version:
<!-- Android: Settings -> Advanced, iOS: Settings -> About -->
<!-- Android: Settings -> Advanced, iOS: Settings -> General -> About -->
### Link to debug log

View file

@ -8,7 +8,7 @@ install:
- yarn install
script:
- yarn run generate
- yarn prepare-build
- yarn prepare-beta-build
- yarn eslint
- yarn test-server
- yarn lint

View file

@ -12,7 +12,7 @@ for it or creating a new one yourself. You can use also that issue as a place to
your intentions and get feedback from the users most likely to appreciate your changes.
You're most likely to have your pull request accepted easily if it addresses bugs already
in the [Next Steps project](https://github.com/WhisperSystems/Signal-Desktop/projects/1),
in the [Next Steps project](https://github.com/signalapp/Signal-Desktop/projects/1),
especially if they are near the top of the Backlog column. Those are what we'll be looking
at next, so it would be a great help if you helped us out!
@ -24,7 +24,7 @@ ounce of prevention, as they say!](https://www.goodreads.com/quotes/247269-an-ou
## Developer Setup
First, you'll need [Node.js](https://nodejs.org/) which matches our current version.
You can check [`.nvmrc` in the `development` branch](https://github.com/WhisperSystems/Signal-Desktop/blob/development/.nvmrc) to see what the current version is. If you have [nvm](https://github.com/creationix/nvm)
You can check [`.nvmrc` in the `development` branch](https://github.com/signalapp/Signal-Desktop/blob/development/.nvmrc) to see what the current version is. If you have [nvm](https://github.com/creationix/nvm)
you can just run `nvm use` in the project directory and it will switch to the project's
desired Node.js version. [nvm for windows](https://github.com/coreybutler/nvm-windows) is
still useful, but it doesn't support `.nvmrc` files.
@ -46,7 +46,7 @@ favorite package manager. Python 2.x and GCC are two key necessary components.
Now, run these commands in your preferred terminal in a good directory for development:
```
git clone https://github.com/WhisperSystems/Signal-Desktop.git
git clone https://github.com/signalapp/Signal-Desktop.git
cd Signal-Desktop
npm install -g yarn # (only if you don't already have yarn)
npm install -g grunt-cli # (only if you don't already have grunt)
@ -215,13 +215,13 @@ and register it with one of your extra phone numbers:
First, build Signal for Android or iOS from source, and point its TextSecure service URL to `textsecure-service-staging.whispersystems.org`:
**on Android:** Replace the `SIGNAL_URL` value in [build.gradle](https://github.com/WhisperSystems/Signal-Android/blob/master/build.gradle)
**on Android:** Replace the `SIGNAL_URL` value in [build.gradle](https://github.com/signalapp/Signal-Android/blob/master/build.gradle)
**on iOS:** Replace the `textSecureServerURL` value in `TSConstants.h`(located in the SignalServiceKit pod)
This task is 1% search and replace, 99% setting up your build environment. Instructions are available for both
the [Android](https://github.com/WhisperSystems/Signal-Android/blob/master/BUILDING.md)
and [iOS](https://github.com/WhisperSystems/Signal-iOS/blob/master/BUILDING.md) projects.
the [Android](https://github.com/signalapp/Signal-Android/blob/master/BUILDING.md)
and [iOS](https://github.com/signalapp/Signal-iOS/blob/master/BUILDING.md) projects.
Then you can set up your development build of Signal Desktop as normal. If you've already
set up as a standalone install, you can switch by opening the DevTools (View -> Toggle

View file

@ -103,6 +103,7 @@ module.exports = function(grunt) {
'!js/Mp3LameEncoder.min.js',
'!js/libsignal-protocol-worker.js',
'!js/components.js',
'!js/modules/**/*.js',
'!js/signal_protocol_store.js',
'_locales/**/*'
],
@ -174,8 +175,10 @@ module.exports = function(grunt) {
'!js/Mp3LameEncoder.min.js',
'!js/libsignal-protocol-worker.js',
'!js/components.js',
'!js/modules/**/*.js',
'test/**/*.js',
'!test/blanket_mocha.js',
'!test/modules/**/*.js',
'!test/test.js',
]
}
@ -282,7 +285,7 @@ module.exports = function(grunt) {
var https = require('https');
var urlBase = "https://s3-us-west-1.amazonaws.com/signal-desktop-builds";
var keyBase = 'WhisperSystems/Signal-Desktop';
var keyBase = 'signalapp/Signal-Desktop';
var sha = gitinfo.local.branch.current.SHA;
var files = [{
zip: packageJson.name + '-' + packageJson.version + '.zip',

View file

@ -1,10 +1,10 @@
[![Build Status](https://travis-ci.org/WhisperSystems/Signal-Desktop.svg?branch=master)](https://travis-ci.org/WhisperSystems/Signal-Desktop)
[![Build Status](https://travis-ci.org/signalapp/Signal-Desktop.svg?branch=master)](https://travis-ci.org/signalapp/Signal-Desktop)
Signal Desktop
==========================
Signal Desktop is an Electron application that links with your
[Signal Android](https://github.com/WhisperSystems/Signal-Android)
or [Signal iOS](https://github.com/WhisperSystems/Signal-iOS) app.
[Signal Android](https://github.com/signalapp/Signal-Android)
or [Signal iOS](https://github.com/signalapp/Signal-iOS) app.
## Install production version: https://signal.org/download/
@ -27,7 +27,7 @@ The discussion groups are another good place for questions: https://whispersyste
Please search the existing issues for your bug and create a new one if the issue is not yet tracked!
https://github.com/WhisperSystems/Signal-Desktop/issues
https://github.com/signalapp/Signal-Desktop/issues
## Contributing Translations
@ -38,7 +38,7 @@ https://www.transifex.com/projects/p/signal-desktop
## Contributing Code
Please see [CONTRIBUTING.md](https://github.com/WhisperSystems/Signal-Desktop/blob/master/CONTRIBUTING.md)
Please see [CONTRIBUTING.md](https://github.com/signalapp/Signal-Desktop/blob/master/CONTRIBUTING.md)
for setup instructions and contributor guidelines. And don't forget to sign the
[CLA](https://signal.org/cla/).

View file

@ -19,79 +19,65 @@
"message": "&Help",
"description": "The label that is used for the Help menu in the program main menu. The '&' indicates that the following letter will be used as the keyboard 'shortcut letter' for accessing the menu with the Alt-<letter> combination."
},
"menuSetupWithImport": {
"message": "Set up with import",
"description": "When the application is not yet set up, menu option to start up the import sequence"
},
"menuSetupAsNewDevice": {
"message": "Set up as new device",
"description": "When the application is not yet set up, menu option to start up the set up as fresh device"
},
"menuSetupAsStandalone": {
"message": "Set up as standalone device",
"description": "Only available on development modes, menu option to open up the standalone device setup sequence"
},
"loading": {
"message": "Loading...",
"description": "Message shown on the loading screen before we've loaded any messages"
},
"exportInstructions": {
"message": "The first step is to choose a directory to store this application's exported data. It will contain your message history and sensitive cryptographic data, so be sure to save it somewhere private.",
"description": "Description of the export process"
},
"chooseDirectory": {
"message": "Choose directory",
"description": "Button to allow the user to export all data from app as part of migration process"
"message": "Choose folder",
"description": "Button to allow the user to find a folder on disk"
},
"exportButton": {
"message": "Export",
"desription": "Button shown on the choose directory dialog which starts the export process"
"loadDataHeader": {
"message": "Load your data",
"description": "Header shown on the first screen in the data import process"
},
"exportChooserTitle": {
"message": "Choose target directory for data",
"description": "Title of the popup window used to select data storage location"
},
"exportAgain": {
"message": "Export again",
"description": "If user has already exported once, this button allows user to do it again if needed"
},
"exportError": {
"message": "Unfortunately, something went wrong during the export. First, double-check your target empty directory for write access and enough space. Then, please submit a debug log so we can help you get migrated!",
"description": "Helper text if the user went forward on migrating the app, but ran into an error"
},
"exporting": {
"message": "Please wait while we export your data. It may take several minutes. You can still use Signal on your phone and other devices during this time.",
"description": "Message shown on the migration screen while we export data"
},
"exportComplete": {
"message": "Your data has been exported to: <p><b>$location$</b></p> You'll be able to import this data as you set up <a target='_blank' href='https://support.whispersystems.org/hc/en-us/articles/214507138'>the new Signal Desktop</a>.",
"description": "Message shown on the migration screen when we are done exporting data",
"placeholders": {
"location": {
"content": "$1",
"example": "/Users/someone/somewhere"
}
}
},
"installNewSignal": {
"message": "Install new Signal Desktop",
"description": "When export is complete, a button shows which sends user to Signal Desktop install instructions"
},
"importButton": {
"message": "Import",
"desription": "Button shown on the choose directory dialog which starts the import process"
"loadDataDescription": {
"message": "You've just gone through the export process, and your contacts and messages are waiting patiently on your computer. Select the folder that contains your saved Signal data.",
"description": "Introduction to the process of importing messages and contacts from disk"
},
"importChooserTitle": {
"message": "Choose directory with exported data",
"description": "Title of the popup window used to select data previously exported"
},
"importErrorHeader": {
"message": "Something went wrong!",
"description": "Header of the error screen after a failed import"
},
"importingHeader": {
"message": "Loading contacts and messages",
"description": "Header of screen shown as data is import"
},
"importError": {
"message": "Unfortunately, something went wrong during the import. <p>First, double-check your target directory. It should start with 'Signal Export.'</p><p>Next, try with a new export of your data from the Chrome App.</p>If that still fails, please <a target='_blank' href='https://support.whispersystems.org/hc/en-us/articles/215188737'>submit a debug log</a> so we can help you get migrated!",
"message": "Make sure you have chosen the correct directory that contains your saved Signal data. Its name should begin with 'Signal Export.' You can also save a new copy of your data from the Chrome App.<p>If these steps don't work for you, please <a target='_blank' href='https://support.signal.org/hc/en-us/articles/215188737'>submit a debug log</a> so that we can help you get migrated!</p>",
"description": "Message shown if the import went wrong."
},
"tryAgain": {
"message": "Try again",
"importAgain": {
"message": "Choose folder and try again",
"description": "Button shown if the user runs into an error during import, allowing them to start over"
},
"importInstructions": {
"message": "The first step is to tell us where you previously <a href='https://support.signal.org/hc/en-us/articles/115002502511'>exported your Signal data</a>. It will be a directory whose name starts with 'Signal Export.'<br><br><b>NOTE</b>: You must only import a set of exported data <b>once</b>. Import makes a copy of the exported client, and duplicate clients interfere with each other.",
"description": "Description of the export process"
"importCompleteHeader": {
"message": "Success!",
"description": "Header shown on the screen at the end of a successful import process"
},
"importing": {
"message": "Please wait while we import your data...",
"description": "Shown as we are loading the user's data from disk"
"importCompleteStartButton": {
"message": "Start using Signal Desktop",
"description": "Button shown at end of successful import process, nothing left but a restart"
},
"importComplete": {
"message": "We've successfully loaded your data. The next step is to restart the application!",
"description": "Shown when the import is complete."
"importCompleteLinkButton": {
"message": "Link this device to your phone",
"description": "Button shown at end of successful 'light' import process, so the standard linking process still needs to happen"
},
"selectedLocation": {
"message": "your selected location",
@ -526,87 +512,46 @@
"message": "Privacy is possible. Signal makes it easy.",
"description": "Tagline displayed under 'installWelcome' string on the install page"
},
"installNew": {
"message": "Set up as new install",
"description": "One of two choices presented on the screen shown on first launch"
"linkYourPhone": {
"message": "Link your phone to Signal Desktop",
"description": "Shown on the front page when the application first starst, above the QR code"
},
"installImport": {
"message": "Set up with exported data",
"description": "One of two choices presented on the screen shown on first launch"
"signalSettings": {
"message": "Signal Settings",
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
},
"installGetStartedButton": {
"message": "Get started"
"linkedDevices": {
"message": "Linked Devices",
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
},
"installSignalLink": {
"message": "First, install <a $a_params$>Signal</a> on your mobile phone. We'll link your devices and keep your messages in sync.",
"description": "Prompt the user to install Signal on their phone before linking",
"placeholders": {
"a_params": {
"content": "$1",
"example": "href='http://example.com'"
}
}
"plusButton": {
"message": "'+' Button",
"description": "The button used in Signal Android to add a new linked device"
},
"installSignalLinks": {
"message": "First, install Signal on your <a $play_store$>Android</a> or <a $app_store$>iPhone</a>.<br /> We'll link your devices and keep your messages in sync.",
"description": "Prompt the user to install Signal on their phone before linking",
"placeholders": {
"play_store": {
"content": "$1",
"example": "href='http://example.com'"
},
"app_store": {
"content": "$2",
"example": "href='http://example.com'"
}
}
"linkNewDevice": {
"message": "Link New Device",
"description": "The menu option shown in Signal iOS to add a new linked device"
},
"installGotIt": {
"message": "Got it",
"description": "Button for the user to confirm that they have Signal installed."
"deviceName": {
"message": "Device name",
"description": "The label in settings panel shown for the user-provided name for this desktop instance"
},
"installIHaveSignalButton": {
"message": "I have Signal for Android",
"description": "Button for the user to confirm that they have Signal for Android"
"chooseDeviceName": {
"message": "Choose this device's name",
"description": "The header shown on the 'choose device name' screen in the device linking process"
},
"installFollowUs": {
"message": "<a $a_params$>Follow us</a> for updates about multi-device support for iOS.",
"placeholders": {
"a_params": {
"content": "$1",
"example": "href='http://example.com'"
}
}
"finishLinkingPhone": {
"message": "Finish linking phone",
"description": "The text on the button to finish the linking process, after choosing the device name"
},
"installAndroidInstructions": {
"message": "Open Signal on your phone and navigate to Settings > Linked devices. Tap the button to add a new device, then scan the code above."
},
"installConnecting": {
"message": "Connecting...",
"description": "Displayed when waiting for the QR Code"
"initialSync": {
"message": "Syncing contacts and groups",
"description": "Shown during initial link while contacts and groups are being pulled from mobile device"
},
"installConnectionFailed": {
"message": "Failed to connect to server.",
"description": "Displayed when we can't connect to the server."
},
"installGeneratingKeys": {
"message": "Generating Keys"
},
"installSyncingGroupsAndContacts": {
"message": "Syncing groups and contacts"
},
"installComputerName": {
"message": "This computer's name will be",
"description": "Text displayed before the input where the user can enter the name for this device."
},
"installLinkingWithNumber": {
"message": "Linking with",
"description": "Text displayed before the phone number that the user is in the process of linking with"
},
"installFinalButton": {
"message": "Looking good",
"description": "The final button for the install process, after the user has entered a name for their device"
},
"installTooManyDevices": {
"message": "Sorry, you have too many devices linked already. Try removing some."
},
@ -748,6 +693,16 @@
}
}
},
"timerSetOnSync": {
"message": "Updating timer to $time$.",
"description": "Message displayed when timer is set on initial link of desktop device.",
"placeholders": {
"time": {
"content": "$1",
"example": "10m"
}
}
},
"theyChangedTheTimer": {
"message": "$name$ set the timer to $time$.",
"description": "Message displayed when someone else changes the message expiration timer in a conversation.",
@ -908,10 +863,10 @@
"message": "There is a new version of Signal available."
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart to apply the updates."
"message": "Press Restart Signal to apply the updates."
},
"autoUpdateRestartButtonLabel": {
"message": "Restart"
"message": "Restart Signal"
},
"autoUpdateLaterButtonLabel": {
"message": "Later"

View file

@ -42,7 +42,7 @@ function showUpdateDialog(mainWindow, messages) {
if (response === RESTART_BUTTON) {
// We delay these update calls because they don't seem to work in this
// callback - but only if the message box has a parent window.
// Fixes this bug: https://github.com/WhisperSystems/Signal-Desktop/issues/1864
// Fixes this bug: https://github.com/signalapp/Signal-Desktop/issues/1864
setTimeout(() => {
windowState.markShouldQuit();
autoUpdater.quitAndInstall();

View file

@ -2,8 +2,14 @@ const path = require('path');
const electronIsDev = require('electron-is-dev');
const defaultEnvironment = electronIsDev ? 'development' : 'production';
const environment = process.env.NODE_ENV || defaultEnvironment;
let environment;
// In production mode, NODE_ENV cannot be customized by the user
if (electronIsDev) {
environment = process.env.NODE_ENV || 'development';
} else {
environment = 'production';
}
// Set environment vars to configure node-config before requiring it
process.env.NODE_ENV = environment;

View file

@ -1,3 +1,6 @@
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
/* eslint-disable more/no-then */
const path = require('path');
const fs = require('fs');
@ -117,8 +120,8 @@ function eliminateOutOfDateFiles(logPath, date) {
const file = {
path: target,
start: isLineAfterDate(start, date),
end: isLineAfterDate(end[end.length - 1], date)
|| isLineAfterDate(end[end.length - 2], date),
end: isLineAfterDate(end[end.length - 1], date) ||
isLineAfterDate(end[end.length - 2], date),
};
if (!file.start && !file.end) {

View file

@ -6,6 +6,9 @@ function createTemplate(options, messages) {
openNewBugForm,
openSupportPage,
openForums,
setupWithImport,
setupAsNewDevice,
setupAsStandalone,
} = options;
const template = [{
@ -123,6 +126,27 @@ function createTemplate(options, messages) {
],
}];
if (options.includeSetup) {
const fileMenu = template[0];
// These are in reverse order, since we're prepending them one at a time
if (options.development) {
fileMenu.submenu.unshift({
label: messages.menuSetupAsStandalone.message,
click: setupAsStandalone,
});
}
fileMenu.submenu.unshift({
label: messages.menuSetupAsNewDevice.message,
click: setupAsNewDevice,
});
fileMenu.submenu.unshift({
label: messages.menuSetupWithImport.message,
click: setupWithImport,
});
}
if (process.platform === 'darwin') {
return updateForMac(template, messages, options);
}
@ -134,14 +158,46 @@ function updateForMac(template, messages, options) {
const {
showWindow,
showAbout,
setupWithImport,
setupAsNewDevice,
setupAsStandalone,
} = options;
// Remove About item and separator from Help menu, since it's on the first menu
template[4].submenu.pop();
template[4].submenu.pop();
// Replace File menu
// Remove File menu
template.shift();
if (options.includeSetup) {
// Add a File menu just for these setup options. Because we're using unshift(), we add
// the file menu first, though it ends up to the right of the Signal Desktop menu.
const fileMenu = {
label: messages.mainMenuFile.message,
submenu: [
{
label: messages.menuSetupWithImport.message,
click: setupWithImport,
},
{
label: messages.menuSetupAsNewDevice.message,
click: setupAsNewDevice,
},
],
};
if (options.development) {
fileMenu.submenu.push({
label: messages.menuSetupAsStandalone.message,
click: setupAsStandalone,
});
}
template.unshift(fileMenu);
}
// Add the OSX-specific Signal Desktop menu at the far left
template.unshift({
submenu: [
{
@ -170,7 +226,8 @@ function updateForMac(template, messages, options) {
});
// Add to Edit menu
template[1].submenu.push(
const editIndex = options.includeSetup ? 2 : 1;
template[editIndex].submenu.push(
{
type: 'separator',
},

View file

@ -19,7 +19,7 @@ build_script:
- node build\grunt.js
- type package.json | findstr /v certificateSubjectName > temp.json
- move temp.json package.json
- yarn prepare-build
- yarn prepare-beta-build
- node_modules\.bin\build --em.environment=%SIGNAL_ENV% --publish=never
test_script:

View file

@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none';
connect-src 'self' wss: https:;
connect-src 'self' https: wss:;
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
@ -536,7 +536,7 @@
</div>
</script>
<script type='text/x-tmpl-mustache' id='link_to_support'>
<a href='http://support.whispersystems.org/hc/articles/213134107' target='_blank'>
<a href='http://support.signal.org/hc/articles/213134107' target='_blank'>
{{ learnMore }}
</a>
</script>
@ -562,7 +562,7 @@
</div>
<p>
<a class='report-link' target='_blank'
href='https://github.com/WhisperSystems/Signal-Desktop/issues/new/'>
href='https://github.com/signalapp/Signal-Desktop/issues/new/'>
{{ reportIssue }}
</a>
</p>
@ -571,6 +571,9 @@
<div class='content'>
<a class='x close' alt='close settings' href='#'></a>
<h2>{{ settings }}</h2>
<div class='device-name-settings'>
<b>{{ deviceNameLabel }}:</b> {{ deviceName }}
</div>
<hr>
<div class='theme-settings'>
<h3>{{ theme }}</h3>
@ -652,151 +655,198 @@
{{/action }}
</script>
<script type='text/x-tmpl-mustache' id='install-choice'>
<div class='step'>
<script type='text/x-tmpl-mustache' id='import-flow-template'>
{{#isStep2}}
<div id='step2' class='step'>
<div class='inner'>
<div class='step-body'>
<img id='signal-icon' src='images/icon_250.png'/>
<h1>{{ installWelcome }}</h1>
<p>{{ installTagline }}</p>
<span class='banner-icon folder-outline'></span>
<div class='header'>{{ chooseHeader }}</div>
<div class='body-text'>{{ choose }}</div>
</div>
<div class='nav'>
<div> <a class='button new'>{{ installNew }}</a> </div>
<div> <a class='button import'>{{ installImport }}</a> </div>
<div>
<a class='button choose'>{{ chooseButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep2}}
{{#isStep3}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon import'></span>
<div class='header'>{{ importingHeader }}</div>
</div>
<div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
</div>
</div>
</div>
</div>
{{/isStep3}}
{{#isStep4}}
<div id='step4' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon check-circle-outline'></span>
<div class='header'>{{ completeHeader }}</div>
</div>
<div class='nav'>
{{#restartButton}}
<div>
<a class='button restart'>{{ restartButton }}</a>
</div>
{{/restartButton}}
{{#registerButton}}
<div>
<a class='button register'>{{ registerButton }}</a>
</div>
{{/registerButton}}
</div>
</div>
</div>
{{/isStep4}}
{{#isError}}
<div id='error' class='step'>
<div class='inner error-dialog clearfix'>
<div class='step-body'>
<span class='banner-icon alert-outline'></span>
<div class='header'>{{ errorHeader }}</div>
<div class='body-text-wide'>{{& errorMessage }}</div>
</div>
<div class='nav'>
<div>
<a class='button choose'>{{ chooseButton }}</a>
</div>
</div>
</div>
</div>
{{/isError}}
</script>
<script type='text/x-tmpl-mustache' id='install_flow_template'>
<div id='step2' class='step hidden'>
<script type='text/x-tmpl-mustache' id='link-flow-template'>
{{#isStep3}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<img id='signal-phone' src='images/signal-phone.png'>
<p>{{{ installSignalLink }}}</p>
<div class='header'>{{ linkYourPhone }}</div>
<div id="qr">
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
</div>
</div>
<div class='nav'>
<div> <a class='button step3'>{{ installIHaveSignalButton }}</a> </div>
<div class='dot-container'>
<span class='dot step1'></span>
<span class='dot step2 selected'></span>
<span class='dot step3'></span>
<div class='instructions'>
<div class='android'>
<div class='label'>
<span class='os-icon android'></span>
</div>
<div class='body'>
<div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div>
<div>→ {{ androidFinalStep }}</div>
</div>
</div>
<div class='apple'>
<div class='label'>
<span class='os-icon apple'></span>
</div>
<div class='body'>
<div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div>
<div>→ {{ appleFinalStep }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id='step3' class='step hidden'>
{{/isStep3}}
{{#isStep4}}
<div id='step4' class='step'>
<div class='inner'>
<div class='step-body'>
<div id="qr"></div>
<p>{{ installAndroidInstructions }}</p>
</div>
<div class='nav'>
{{ #development }}
<div> <a class='button openStandalone'>Standalone</a> </div>
{{ /development }}
<div class='dot-container'>
<span class='dot step1'></span>
<span class='dot step2'></span>
<span class='dot step3 selected'></span>
</div>
</div>
</div>
</div>
<form id='step4' class='step hidden'>
<div class='inner'>
<div class='step-body'>
<p>{{ installLinkingWithNumber }}</p>
<h2 class='number'></h2>
<img id='signal-computer' src='images/signal-laptop.png'>
<p>{{ installComputerName }}</p>
<span class='banner-icon lead-pencil'></span>
<div class='header'>{{ chooseName }}</div>
<div>
<input type='text' id='device-name' spellcheck='false' maxlength='50' />
<input type='text' class='device-name' spellcheck='false' maxlength='50' />
</div>
</div>
<div class='nav'>
<div>
<input type='submit' class='button' id='sync' value='{{ installFinalButton }}' />
<a class='button finish'>{{ finishLinkingPhoneButton }}</a>
</div>
</div>
</div>
</form>
<div id='step5' class='step hidden'>
</div>
{{/isStep4}}
{{#isStep5}}
<div id='step5' class='step'>
<div class='inner'>
<div class='step-body'>
<img id='signal-icon' src='images/icon_250.png'/>
<div class='progress-dialog'>
<p class='status'></p>
<div class='bar-container'><div class='bar progress-bar'></div></div>
<span class='banner-icon sync'></span>
<div class='header'>{{ syncing }}</div>
</div>
<div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
</div>
</div>
<div class='nav'>
</div>
</div>
</div>
{{/isStep5}}
<div id='stepTooManyDevices' class='step hidden'>
<div class='inner error-dialog clearfix'>
<div class='panel step-body'>{{ installTooManyDevices }}</div>
<div class='nav'>
<button class='ok step3'>{{ ok }}</button>
</div>
</div>
</div>
<div id='stepNetworkError' class='step hidden'>
<div class='inner error-dialog clearfix'>
<div class='panel step-body'>{{ installConnectionFailed }}</div>
<button class='ok step3 button'>{{ tryAgain }}</button>
{{#isError}}
<div id='error' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon alert-outline'></span>
<div class='header'>{{ errorHeader }}</div>
<div class='body'>{{ errorMessage }}</div>
</div>
<div class='nav'>
<a class='button try-again'>{{ errorButton }}</a>
</div>
</div>
</div>
{{/isError}}
</script>
<script type='text/x-tmpl-mustache' id='standalone'>
<header>
<div class='container'>
<div class='row'>
<div class='col-xs-2 col-md-1'>
<img id='textsecure-icon' src='images/icon_250.png'/>
</div>
<div class='col-xs-10 col-md-11'>
<h1>Create your Signal Account</h1>
<h4 class='tagline'>Private messaging from your web browser.</h4>
</div>
</div>
</div>
</header>
<div class='container'>
<div class='col-xs-offset-1 col-md-6'>
<div class='narrow'>
<div id="phone-number-input">
<div class="phone-input-form">
<div id="number-container" class="number-container">
<input type="tel" class="number" placeholder="Phone Number" />
<div class='step'>
<div class='inner'>
<div class='step-body'>
<img class='banner-image' src='images/icon_128.png' />
<div class='header'>Create your Signal Account</div>
<div id='phone-number-input'>
<div class='phone-input-form'>
<div id='number-container' class='number-container'>
<input type='tel' class='number' placeholder='Phone Number' />
</div>
</div>
</div>
<div class='clearfix'>
<button id="request-sms" class="btn btn-info">Send SMS</button>
<button id="request-voice" class="btn btn-info" tabindex=-1>Call</button>
<a class='button' id='request-sms'>Send SMS</a>
<a class='link' id='request-voice' tabindex=-1>Call</a>
</div>
<form id='form'>
<h2></h2>
<input class='form-control' type="text" pattern="\s*[0-9]{3}-?[0-9]{3}\s*" title="Enter your 6-digit verification code. If you did not receive a code, click Call or Send SMS to request a new one" id="code" placeholder="Verification Code" autocomplete='off'>
<button id="verifyCode" class="btn btn-info" data-loading-text="Please wait...">Register</button>
<div id='error' class='collapse'></div>
<div> <a class='button openInstaller'>Link to phone</a> </div>
</form>
<input class='form-control' type='text' pattern='\s*[0-9]{3}-?[0-9]{3}\s*' title='Enter your 6-digit verification code. If you did not receive a code, click Call or Send SMS to request a new one' id='code' placeholder='Verification Code' autocomplete='off'>
<div id='error' class='collapse'></div>
<div id=status></div>
</div>
<div class='nav'>
<a class='button' id='verifyCode' data-loading-text='Please wait...'>Register</a>
</div>
</div>
</div>
</script>
<script type='text/javascript' src='js/components.js'></script>
<script type='text/javascript' src='js/reliable_trigger.js'></script>
<script type='text/javascript' src='js/database.js'></script>
@ -857,7 +907,6 @@
<script type="text/javascript" src="js/views/phone-input-view.js"></script>
<script type='text/javascript' src='js/views/standalone_registration_view.js'></script>
<script type='text/javascript' src='js/views/app_view.js'></script>
<script type='text/javascript' src='js/views/install_choice_view.js'></script>
<script type='text/javascript' src='js/views/import_view.js'></script>
<script type='text/javascript' src='js/wall_clock_listener.js'></script>

View file

@ -1,7 +1,7 @@
{
"name": "textsecure-chrome",
"name": "signal-desktop",
"version": "0.0.0",
"homepage": "https://github.com/WhisperSystems/TextSecure-Browser",
"homepage": "https://github.com/signalapp/Signal-Desktop",
"license": "GPLV3",
"private": true,
"dependencies": {
@ -15,7 +15,6 @@
"indexeddb-backbonejs-adapter": "*",
"intl-tel-input": "~4.0.1",
"blueimp-load-image": "~1.13.0",
"blueimp-canvas-to-blob": "~2.1.1",
"autosize": "~4.0.0",
"webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git",
"mp3lameencoder": "https://github.com/higuma/mp3-lame-encoder-js.git",
@ -69,12 +68,6 @@
"build/img/flags.png",
"build/js/intlTelInput.js"
],
"blueimp-load-image": [
"js/load-image.js"
],
"blueimp-canvas-to-blob": [
"js/canvas-to-blob.js"
],
"emojijs": [
"lib/emoji.js",
"demo/emoji.css"
@ -113,8 +106,6 @@
"moment",
"intl-tel-input",
"backbone.typeahead",
"blueimp-load-image",
"blueimp-canvas-to-blob",
"autosize",
"filesize"
],

View file

@ -4,5 +4,8 @@
"disableAutoUpdate": false,
"openDevTools": false,
"buildExpiration": 0,
"certificateAuthorities": ["-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n"]
"certificateAuthorities": [
"-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n"
],
"import": false
}

1
images/alert-outline.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16" /></svg>

After

Width:  |  Height:  |  Size: 357 B

1
images/android.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M15,5H14V4H15M10,5H9V4H10M15.53,2.16L16.84,0.85C17.03,0.66 17.03,0.34 16.84,0.14C16.64,-0.05 16.32,-0.05 16.13,0.14L14.65,1.62C13.85,1.23 12.95,1 12,1C11.04,1 10.14,1.23 9.34,1.63L7.85,0.14C7.66,-0.05 7.34,-0.05 7.15,0.14C6.95,0.34 6.95,0.66 7.15,0.85L8.46,2.16C6.97,3.26 6,5 6,7H18C18,5 17,3.25 15.53,2.16M20.5,8A1.5,1.5 0 0,0 19,9.5V16.5A1.5,1.5 0 0,0 20.5,18A1.5,1.5 0 0,0 22,16.5V9.5A1.5,1.5 0 0,0 20.5,8M3.5,8A1.5,1.5 0 0,0 2,9.5V16.5A1.5,1.5 0 0,0 3.5,18A1.5,1.5 0 0,0 5,16.5V9.5A1.5,1.5 0 0,0 3.5,8M6,18A1,1 0 0,0 7,19H8V22.5A1.5,1.5 0 0,0 9.5,24A1.5,1.5 0 0,0 11,22.5V19H13V22.5A1.5,1.5 0 0,0 14.5,24A1.5,1.5 0 0,0 16,22.5V19H17A1,1 0 0,0 18,18V8H6V18Z" /></svg>

After

Width:  |  Height:  |  Size: 955 B

1
images/apple.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M18.71,19.5C17.88,20.74 17,21.95 15.66,21.97C14.32,22 13.89,21.18 12.37,21.18C10.84,21.18 10.37,21.95 9.1,22C7.79,22.05 6.8,20.68 5.96,19.47C4.25,17 2.94,12.45 4.7,9.39C5.57,7.87 7.13,6.91 8.82,6.88C10.1,6.86 11.32,7.75 12.11,7.75C12.89,7.75 14.37,6.68 15.92,6.84C16.57,6.87 18.39,7.1 19.56,8.82C19.47,8.88 17.39,10.1 17.41,12.63C17.44,15.65 20.06,16.66 20.09,16.67C20.06,16.74 19.67,18.11 18.71,19.5M13,3.5C13.73,2.67 14.94,2.04 15.94,2C16.07,3.17 15.6,4.35 14.9,5.19C14.21,6.04 13.07,6.7 11.95,6.61C11.8,5.46 12.36,4.26 13,3.5Z" /></svg>

After

Width:  |  Height:  |  Size: 824 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z" /></svg>

After

Width:  |  Height:  |  Size: 499 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z" /></svg>

After

Width:  |  Height:  |  Size: 401 B

1
images/import.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z" /></svg>

After

Width:  |  Height:  |  Size: 421 B

1
images/lead-pencil.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M16.84,2.73C16.45,2.73 16.07,2.88 15.77,3.17L13.65,5.29L18.95,10.6L21.07,8.5C21.67,7.89 21.67,6.94 21.07,6.36L17.9,3.17C17.6,2.88 17.22,2.73 16.84,2.73M12.94,6L4.84,14.11L7.4,14.39L7.58,16.68L9.86,16.85L10.15,19.41L18.25,11.3M4.25,15.04L2.5,21.73L9.2,19.94L8.96,17.78L6.65,17.61L6.47,15.29" /></svg>

After

Width:  |  Height:  |  Size: 584 B

1
images/sync.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" /></svg>

After

Width:  |  Height:  |  Size: 521 B

6
js/.eslintrc Normal file
View file

@ -0,0 +1,6 @@
{
"env": {
"browser": true,
"node": false
},
}

View file

@ -1,13 +1,35 @@
/*
* vim: ts=4:sw=4:expandtab
*/
/* eslint-disable */
/* eslint-env browser */
/* global Backbone: false */
/* global $: false */
/* global ConversationController: false */
/* global getAccountManager: false */
/* global Signal: false */
/* global storage: false */
/* global textsecure: false */
/* global Whisper: false */
/* global wrapDeferred: false */
;(function() {
'use strict';
const { Message } = window.Signal.Types;
// Implicitly used in `indexeddb-backbonejs-adapter`:
// https://github.com/signalapp/Signal-Desktop/blob/4033a9f8137e62ed286170ed5d4941982b1d3a64/components/indexeddb-backbonejs-adapter/backbone-indexeddb.js#L569
window.onInvalidStateError = function(e) {
console.log(e);
};
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);
@ -85,6 +107,27 @@
}
});
Whisper.events.on('setupWithImport', function() {
var appView = window.owsDesktopApp.appView;
if (appView) {
appView.openImporter();
}
});
Whisper.events.on('setupAsNewDevice', function() {
var appView = window.owsDesktopApp.appView;
if (appView) {
appView.openInstaller();
}
});
Whisper.events.on('setupAsStandalone', function() {
var appView = window.owsDesktopApp.appView;
if (appView) {
appView.openStandalone();
}
});
function start() {
var currentVersion = window.config.version;
var lastVersion = storage.get('version');
@ -118,8 +161,10 @@
appView.openInbox({
initialLoadComplete: initialLoadComplete
});
} else if (window.config.importMode) {
appView.openImporter();
} else {
appView.openInstallChoice();
appView.openInstaller();
}
Whisper.events.on('showDebugLog', function() {
@ -136,12 +181,6 @@
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) {
@ -266,7 +305,7 @@
messageReceiver.addEventListener('error', onError);
messageReceiver.addEventListener('empty', onEmpty);
messageReceiver.addEventListener('progress', onProgress);
messageReceiver.addEventListener('settings', onSettings);
messageReceiver.addEventListener('configuration', onConfiguration);
window.textsecure.messaging = new textsecure.MessageSender(
SERVER_URL, USERNAME, PASSWORD, CDN_URL
@ -345,12 +384,8 @@
view.onProgress(count);
}
}
function onSettings(ev) {
if (ev.settings.readReceipts) {
storage.put('read-receipt-setting', true);
} else {
storage.put('read-receipt-setting', false);
}
function onConfiguration(ev) {
storage.put('read-receipt-setting', ev.configuration.readReceipts);
}
function onContactReceived(ev) {
@ -377,38 +412,58 @@
return ConversationController.getOrCreateAndWait(id, 'private')
.then(function(conversation) {
return new Promise(function(resolve, reject) {
var activeAt = conversation.get('active_at');
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();
}
// 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 (details.profileKey) {
conversation.set({profileKey: details.profileKey});
}
if (typeof details.blocked !== 'undefined') {
if (details.blocked) {
storage.addBlockedNumber(id);
} else {
storage.removeBlockedNumber(id);
}
conversation.save({
name: details.name,
avatar: details.avatar,
color: details.color,
active_at: activeAt,
}).then(resolve, reject);
}).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);
}
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(
@ -437,93 +492,138 @@
if (activeAt !== null) {
updates.active_at = activeAt || Date.now();
}
updates.left = false;
} else {
updates.left = true;
}
return new Promise(function(resolve, reject) {
conversation.save(updates).then(resolve, reject);
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);
});
}
function onMessageReceived(ev) {
var data = ev.data;
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
var profileKey = data.message.profileKey.toArrayBuffer();
return ConversationController.getOrCreateAndWait(data.source, 'private').then(function(sender) {
return sender.setProfileKey(profileKey).then(ev.confirm);
});
}
var message = initIncomingMessage(data);
/* eslint-enable */
/* jshint ignore:start */
return isMessageDuplicate(message).then(function(isDuplicate) {
if (isDuplicate) {
console.log('Received duplicate message', message.idForLogging());
ev.confirm();
return;
}
// Descriptors
const getGroupDescriptor = group => ({
type: Message.GROUP,
id: group.id,
});
var type, id;
if (data.message.group) {
type = 'group';
id = data.message.group.id;
} else {
type = 'private';
id = data.source;
}
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
const getDescriptorForSent = ({ message, destination }) => (
message.group
? getGroupDescriptor(message.group)
: { type: Message.PRIVATE, id: destination }
);
return ConversationController.getOrCreateAndWait(id, type).then(function() {
return message.handleDataMessage(data.message, ev.confirm, {
initialLoadComplete: initialLoadComplete
});
});
});
}
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
const getDescriptorForReceived = ({ message, source }) => (
message.group
? getGroupDescriptor(message.group)
: { type: Message.PRIVATE, id: source }
);
function onSentMessage(ev) {
var now = new Date().getTime();
var data = ev.data;
function createMessageHandler({
createMessage,
getMessageDescriptor,
handleProfileUpdate,
}) {
return async (event) => {
const { data, confirm } = event;
var type, id;
if (data.message.group) {
type = 'group';
id = data.message.group.id;
} else {
type = 'private';
id = data.destination;
}
const messageDescriptor = getMessageDescriptor(data);
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
return ConversationController.getOrCreateAndWait(id, type).then(function(convo) {
return convo.save({profileSharing: true}).then(ev.confirm);
});
}
const { PROFILE_KEY_UPDATE } = textsecure.protobuf.DataMessage.Flags;
// eslint-disable-next-line no-bitwise
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
if (isProfileUpdate) {
return handleProfileUpdate({ data, confirm, messageDescriptor });
}
var message = new Whisper.Message({
source : textsecure.storage.user.getNumber(),
sourceDevice : data.device,
sent_at : data.timestamp,
received_at : now,
conversationId : data.destination,
type : 'outgoing',
sent : true,
expirationStartTimestamp: data.expirationStartTimestamp,
});
const message = createMessage(data);
const isDuplicate = await isMessageDuplicate(message);
if (isDuplicate) {
console.log('Received duplicate message', message.idForLogging());
return event.confirm();
}
return isMessageDuplicate(message).then(function(isDuplicate) {
if (isDuplicate) {
console.log('Received duplicate message', message.idForLogging());
ev.confirm();
return;
}
const upgradedMessage = await Message.upgradeSchema(data.message);
await ConversationController.getOrCreateAndWait(
messageDescriptor.id,
messageDescriptor.type
);
return message.handleDataMessage(
upgradedMessage,
event.confirm,
{ initialLoadComplete }
);
};
}
return ConversationController.getOrCreateAndWait(id, type).then(function() {
return message.handleDataMessage(data.message, ev.confirm, {
initialLoadComplete: initialLoadComplete
});
});
});
}
// Received:
async function handleMessageReceivedProfileUpdate({
data,
confirm,
messageDescriptor,
}) {
const profileKey = data.message.profileKey.toArrayBuffer();
const sender = await ConversationController.getOrCreateAndWait(
messageDescriptor.id,
'private'
);
await sender.setProfileKey(profileKey);
return confirm();
}
const onMessageReceived = createMessageHandler({
handleProfileUpdate: handleMessageReceivedProfileUpdate,
getMessageDescriptor: getDescriptorForReceived,
createMessage: initIncomingMessage,
});
// Sent:
async function handleMessageSentProfileUpdate({ confirm, messageDescriptor }) {
const conversation = await ConversationController.getOrCreateAndWait(
messageDescriptor.id,
messageDescriptor.type
);
await conversation.save({ profileSharing: true });
return confirm();
}
function createSentMessage(data) {
const now = Date.now();
return new Whisper.Message({
source: textsecure.storage.user.getNumber(),
sourceDevice: data.device,
sent_at: data.timestamp,
received_at: now,
conversationId: data.destination,
type: 'outgoing',
sent: true,
expirationStartTimestamp: data.expirationStartTimestamp,
});
}
const onSentMessage = createMessageHandler({
handleProfileUpdate: handleMessageSentProfileUpdate,
getMessageDescriptor: getDescriptorForSent,
createMessage: createSentMessage,
});
/* jshint ignore:end */
/* eslint-disable */
function isMessageDuplicate(message) {
return new Promise(function(resolve) {

View file

@ -75,9 +75,9 @@
};
}
function exportNonMessages(idb_db, parent) {
function exportNonMessages(idb_db, parent, options) {
return createFileAndWriter(parent, 'db.json').then(function(writer) {
return exportToJsonFile(idb_db, writer);
return exportToJsonFile(idb_db, writer, options);
});
}
@ -85,10 +85,27 @@
* Export all data from an IndexedDB database
* @param {IDBDatabase} idb_db
*/
function exportToJsonFile(idb_db, fileWriter) {
function exportToJsonFile(idb_db, fileWriter, options) {
options = options || {};
_.defaults(options, {excludeClientConfig: false});
return new Promise(function(resolve, reject) {
var storeNames = idb_db.objectStoreNames;
storeNames = _.without(storeNames, 'messages');
if (options.excludeClientConfig) {
console.log('exportToJsonFile: excluding client config from export');
storeNames = _.without(
storeNames,
'items',
'signedPreKeys',
'preKeys',
'identityKeys',
'sessions',
'unprocessed' // since we won't be able to decrypt them anyway
);
}
var exportedStoreNames = [];
if (storeNames.length === 0) {
throw new Error('No stores to export');
@ -160,9 +177,10 @@
});
}
function importNonMessages(idb_db, parent) {
return readFileAsText(parent, 'db.json').then(function(string) {
return importFromJsonString(idb_db, string);
function importNonMessages(idb_db, parent, options) {
var file = 'db.json';
return readFileAsText(parent, file).then(function(string) {
return importFromJsonString(idb_db, string, path.join(parent, file), options);
});
}
@ -176,6 +194,16 @@
reject(error || new Error(prefix));
}
function eliminateClientConfigInBackup(data, path) {
var cleaned = _.pick(data, 'conversations', 'groups');
console.log('Writing configuration-free backup file back to disk');
try {
fs.writeFileSync(path, JSON.stringify(cleaned));
} catch (error) {
console.log('Error writing cleaned-up backup to disk: ', error.stack);
}
}
/**
* Import data from JSON into an IndexedDB database. This does not delete any existing data
* from the database, so keys could clash
@ -183,19 +211,50 @@
* @param {IDBDatabase} idb_db
* @param {string} jsonString - data to import, one key per object store
*/
function importFromJsonString(idb_db, jsonString) {
function importFromJsonString(idb_db, jsonString, path, options) {
options = options || {};
_.defaults(options, {
forceLightImport: false,
conversationLookup: {},
groupLookup: {},
});
var conversationLookup = options.conversationLookup;
var groupLookup = options.groupLookup;
var result = {
fullImport: true,
};
return new Promise(function(resolve, reject) {
var importObject = JSON.parse(jsonString);
delete importObject.debug;
var storeNames = _.keys(importObject);
if (!importObject.sessions || options.forceLightImport) {
result.fullImport = false;
delete importObject.items;
delete importObject.signedPreKeys;
delete importObject.preKeys;
delete importObject.identityKeys;
delete importObject.sessions;
delete importObject.unprocessed;
console.log('This is a light import; contacts, groups and messages only');
}
// We mutate the on-disk backup to prevent the user from importing client
// configuration more than once - that causes lots of encryption errors.
// This of course preserves the true data: conversations and groups.
eliminateClientConfigInBackup(importObject, path);
var storeNames = _.keys(importObject);
console.log('Importing to these stores:', storeNames.join(', '));
var finished = false;
var finish = function(via) {
console.log('non-messages import done via', via);
if (finished) {
resolve();
resolve(result);
}
finished = true;
};
@ -219,20 +278,46 @@
}
var count = 0;
var skipCount = 0;
var finishStore = function() {
// added all objects for this store
delete importObject[storeName];
console.log(
'Done importing to store',
storeName,
'Total count:',
count,
'Skipped:',
skipCount
);
if (_.keys(importObject).length === 0) {
// added all object stores
console.log('DB import complete');
finish('puts scheduled');
}
};
_.each(importObject[storeName], function(toAdd) {
toAdd = unstringify(toAdd);
var haveConversationAlready =
storeName === 'conversations'
&& conversationLookup[getConversationKey(toAdd)];
var haveGroupAlready =
storeName === 'groups' && groupLookup[getGroupKey(toAdd)];
if (haveConversationAlready || haveGroupAlready) {
skipCount++;
count++;
return;
}
var request = transaction.objectStore(storeName).put(toAdd, toAdd.id);
request.onsuccess = function(event) {
count++;
if (count == importObject[storeName].length) {
// added all objects for this store
delete importObject[storeName];
console.log('Done importing to store', storeName);
if (_.keys(importObject).length === 0) {
// added all object stores
console.log('DB import complete');
finish('puts scheduled');
}
finishStore();
}
};
request.onerror = function(e) {
@ -243,6 +328,12 @@
);
};
});
// We have to check here, because we may have skipped every item, resulting
// in no onsuccess callback at all.
if (count === importObject[storeName].length) {
finishStore();
}
});
});
}
@ -432,14 +523,20 @@
request.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
if (count !== 0) {
stream.write(',');
}
var message = cursor.value;
var messageId = message.received_at;
var attachments = message.attachments;
// skip message if it is disappearing, no matter the amount of time left
if (message.expireTimer) {
cursor.continue();
return;
}
if (count !== 0) {
stream.write(',');
}
message.attachments = _.map(attachments, function(attachment) {
return _.omit(attachment, ['data']);
});
@ -598,6 +695,10 @@
}));
}
function saveMessage(idb_db, message) {
return saveAllMessages(idb_db, [message]);
}
function saveAllMessages(idb_db, messages) {
if (!messages.length) {
return Promise.resolve();
@ -658,43 +759,64 @@
// message, save it, and only then do we move on to the next message. Thus, every
// message with attachments needs to be removed from our overall message save with the
// filter() call.
function importConversation(idb_db, dir) {
function importConversation(idb_db, dir, options) {
options = options || {};
_.defaults(options, {messageLookup: {}});
var messageLookup = options.messageLookup;
var conversationId = 'unknown';
var total = 0;
var skipped = 0;
return readFileAsText(dir, 'messages.json').then(function(contents) {
var promiseChain = Promise.resolve();
var json = JSON.parse(contents);
var conversationId;
if (json.messages && json.messages.length) {
conversationId = json.messages[0].conversationId;
conversationId = '[REDACTED]' + (json.messages[0].conversationId || '').slice(-3);
}
total = json.messages.length;
var messages = _.filter(json.messages, function(message) {
message = unstringify(message);
if (messageLookup[getMessageKey(message)]) {
skipped++;
return false;
}
if (message.attachments && message.attachments.length) {
var process = function() {
return loadAttachments(dir, message).then(function() {
return saveAllMessages(idb_db, [message]);
return saveMessage(idb_db, message);
});
};
promiseChain = promiseChain.then(process);
return null;
return false;
}
return message;
return true;
});
return saveAllMessages(idb_db, messages)
var promise = Promise.resolve();
if (messages.length > 0) {
promise = saveAllMessages(idb_db, messages);
}
return promise
.then(function() {
return promiseChain;
})
.then(function() {
console.log(
'Finished importing conversation',
// Don't know if group or private conversation, so we blindly redact
conversationId ? '[REDACTED]' + conversationId.slice(-3) : 'with no messages'
conversationId,
'Total:',
total,
'Skipped:',
skipped
);
});
@ -703,7 +825,7 @@
});
}
function importConversations(idb_db, dir) {
function importConversations(idb_db, dir, options) {
return getDirContents(dir).then(function(contents) {
var promiseChain = Promise.resolve();
@ -713,7 +835,7 @@
}
var process = function() {
return importConversation(idb_db, conversationDir);
return importConversation(idb_db, conversationDir, options);
};
promiseChain = promiseChain.then(process);
@ -723,6 +845,73 @@
});
}
function getMessageKey(message) {
var ourNumber = textsecure.storage.user.getNumber();
var source = message.source || ourNumber;
if (source === ourNumber) {
return source + ' ' + message.timestamp;
}
var sourceDevice = message.sourceDevice || 1;
return source + '.' + sourceDevice + ' ' + message.timestamp;
}
function loadMessagesLookup(idb_db) {
return assembleLookup(idb_db, 'messages', getMessageKey);
}
function getConversationKey(conversation) {
return conversation.id;
}
function loadConversationLookup(idb_db) {
return assembleLookup(idb_db, 'conversations', getConversationKey);
}
function getGroupKey(group) {
return group.id;
}
function loadGroupsLookup(idb_db) {
return assembleLookup(idb_db, 'groups', getGroupKey);
}
function assembleLookup(idb_db, storeName, keyFunction) {
var lookup = Object.create(null);
return new Promise(function(resolve, reject) {
var transaction = idb_db.transaction(storeName, 'readwrite');
transaction.onerror = function(e) {
handleDOMException(
'assembleLookup(' + storeName + ') transaction error',
transaction.error,
reject
);
};
transaction.oncomplete = function() {
// not really very useful - fires at unexpected times
};
var promiseChain = Promise.resolve();
var store = transaction.objectStore(storeName);
var request = store.openCursor();
request.onerror = function(e) {
handleDOMException(
'assembleLookup(' + storeName + ') request error',
request.error,
reject
);
};
request.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor && cursor.value) {
lookup[keyFunction(cursor.value)] = true;
cursor.continue();
} else {
console.log('Done creating ' + storeName + ' lookup');
return resolve(lookup);
}
};
});
}
function clearAllStores(idb_db) {
return new Promise(function(resolve, reject) {
console.log('Clearing all indexeddb stores');
@ -791,7 +980,7 @@
};
return getDirectory(options);
},
backupToDirectory: function(directory) {
exportToDirectory: function(directory, options) {
var dir;
var idb;
return openDatabase().then(function(idb_db) {
@ -800,7 +989,7 @@
return createDirectory(directory, name);
}).then(function(created) {
dir = created;
return exportNonMessages(idb, dir);
return exportNonMessages(idb, dir, options);
}).then(function() {
return exportConversations(idb, dir);
}).then(function() {
@ -823,18 +1012,30 @@
};
return getDirectory(options);
},
importFromDirectory: function(directory) {
var idb;
importFromDirectory: function(directory, options) {
options = options || {};
var idb, nonMessageResult;
return openDatabase().then(function(idb_db) {
idb = idb_db;
return importNonMessages(idb_db, directory);
return Promise.all([
loadMessagesLookup(idb_db),
loadConversationLookup(idb_db),
loadGroupsLookup(idb_db),
]);
}).then(function(lookups) {
options.messageLookup = lookups[0];
options.conversationLookup = lookups[1];
options.groupLookup = lookups[2];
}).then(function() {
return importConversations(idb, directory);
return importNonMessages(idb, directory, options);
}).then(function(result) {
nonMessageResult = result;
return importConversations(idb, directory, options);
}).then(function() {
return directory;
}).then(function(path) {
console.log('done restoring from backup!');
return path;
return nonMessageResult;
}, function(error) {
console.log(
'the import went wrong:',

View file

@ -3,8 +3,27 @@
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
storage.isBlocked = function(number) {
return storage.get('blocked', []).indexOf(number) >= 0;
var numbers = storage.get('blocked', []);
return _.include(numbers, number);
};
storage.addBlockedNumber = function(number) {
var numbers = storage.get('blocked', []);
if (_.include(numbers, number)) {
return;
}
console.log('adding', number, 'to blocked list');
storage.put('blocked', numbers.concat(number));
};
storage.removeBlockedNumber = function(number) {
var numbers = storage.get('blocked', []);
if (!_.include(numbers, number)) {
return;
}
console.log('removing', number, 'from blocked list');
storage.put('blocked', _.without(numbers, number));
};
})();

View file

@ -1,10 +1,16 @@
/*
* vim: ts=4:sw=4:expandtab
*/
/* eslint-disable */
/* global Signal: false */
/* global storage: false */
/* global textsecure: false */
/* global Whisper: false */
(function () {
'use strict';
window.Whisper = window.Whisper || {};
const { Attachment, Message } = window.Signal.Types;
// TODO: Factor out private and group subclasses of Conversation
var COLORS = [
@ -32,13 +38,13 @@
if (ab1.byteLength !== ab2.byteLength) {
return false;
}
var result = true;
var result = 0;
var ta1 = new Uint8Array(ab1);
var ta2 = new Uint8Array(ab2);
for (var i = 0; i < ab1.byteLength; ++i) {
if (ta1[i] !== ta2[i]) { result = false; }
result = result | ta1[i] ^ ta2[i];
}
return result;
return result === 0;
}
Whisper.Conversation = Backbone.Model.extend({
@ -598,54 +604,71 @@
}
},
sendMessage: function(body, attachments) {
this.queueJob(function() {
var now = Date.now();
/* jshint ignore:start */
/* eslint-enable */
sendMessage(body, attachments) {
this.queueJob(async () => {
const now = Date.now();
console.log(
'Sending message to conversation',
this.idForLogging(),
'with timestamp',
now
);
console.log(
'Sending message to conversation',
this.idForLogging(),
'with timestamp',
now
);
var message = this.messageCollection.add({
body : body,
conversationId : this.id,
type : 'outgoing',
attachments : attachments,
sent_at : now,
received_at : now,
expireTimer : this.get('expireTimer'),
recipients : this.getRecipients()
});
if (this.isPrivate()) {
message.set({destination: this.id});
}
message.save();
const upgradedAttachments =
await Promise.all(attachments.map(Attachment.upgradeSchema));
const message = this.messageCollection.add({
body,
conversationId: this.id,
type: 'outgoing',
attachments: upgradedAttachments,
sent_at: now,
received_at: now,
expireTimer: this.get('expireTimer'),
recipients: this.getRecipients(),
});
if (this.isPrivate()) {
message.set({ destination: this.id });
}
message.save();
this.save({
active_at : now,
timestamp : now,
lastMessage : message.getNotificationText()
});
this.save({
active_at: now,
timestamp: now,
lastMessage: message.getNotificationText(),
});
var sendFunc;
if (this.get('type') == 'private') {
sendFunc = textsecure.messaging.sendMessageToNumber;
}
else {
sendFunc = textsecure.messaging.sendMessageToGroup;
}
const conversationType = this.get('type');
const sendFunc = (() => {
switch (conversationType) {
case Message.PRIVATE:
return textsecure.messaging.sendMessageToNumber;
case Message.GROUP:
return textsecure.messaging.sendMessageToGroup;
default:
throw new TypeError(`Invalid conversation type: '${conversationType}'`);
}
})();
var profileKey;
if (this.get('profileSharing')) {
profileKey = storage.get('profileKey');
}
let profileKey;
if (this.get('profileSharing')) {
profileKey = storage.get('profileKey');
}
message.send(sendFunc(this.get('id'), body, attachments, now, this.get('expireTimer'), profileKey));
}.bind(this));
message.send(sendFunc(
this.get('id'),
body,
upgradedAttachments,
now,
this.get('expireTimer'),
profileKey
));
});
},
/* jshint ignore:end */
/* eslint-disable */
updateLastMessage: function() {
var collection = new Whisper.MessageCollection();
@ -668,11 +691,28 @@
}.bind(this));
},
updateExpirationTimer: function(expireTimer, source, received_at) {
if (!expireTimer) { expireTimer = null; }
updateExpirationTimer: function(expireTimer, source, received_at, options) {
options = options || {};
_.defaults(options, {fromSync: false});
if (!expireTimer) {
expireTimer = null;
}
if (this.get('expireTimer') === expireTimer
|| (!expireTimer && !this.get('expireTimer'))) {
return;
}
console.log(
'Updating expireTimer for conversation',
this.idForLogging(),
'via',
source
);
source = source || textsecure.storage.user.getNumber();
var timestamp = received_at || Date.now();
this.save({ expireTimer: expireTimer });
var message = this.messageCollection.add({
conversationId : this.id,
type : received_at ? 'incoming' : 'outgoing',
@ -681,7 +721,8 @@
flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
expirationTimerUpdate : {
expireTimer : expireTimer,
source : source
source : source,
fromSync : options.fromSync,
}
});
if (this.isPrivate()) {
@ -690,8 +731,16 @@
if (message.isOutgoing()) {
message.set({recipients: this.getRecipients() });
}
message.save();
if (message.isOutgoing()) { // outgoing update, send it to the number/group
return Promise.all([
wrapDeferred(message.save()),
wrapDeferred(this.save({ expireTimer: expireTimer })),
]).then(function() {
if (message.isIncoming()) {
return message;
}
// change was made locally, send it to the number/group
var sendFunc;
if (this.get('type') == 'private') {
sendFunc = textsecure.messaging.sendExpirationTimerUpdateToNumber;
@ -703,9 +752,16 @@
if (this.get('profileSharing')) {
profileKey = storage.get('profileKey');
}
message.send(sendFunc(this.get('id'), this.get('expireTimer'), message.get('sent_at'), profileKey));
}
return message;
var promise = sendFunc(this.get('id'),
this.get('expireTimer'),
message.get('sent_at'),
profileKey
);
return message.send(promise).then(function() {
return message;
});
}.bind(this));
},
isSearchable: function() {
@ -855,6 +911,11 @@
},
getProfile: function(id) {
if (!textsecure.messaging) {
var message = 'Conversation.getProfile: textsecure.messaging not available';
return Promise.reject(new Error(message));
}
return textsecure.messaging.getProfile(id).then(function(profile) {
var identityKey = dcodeIO.ByteBuffer.wrap(profile.identityKey, 'base64').toArrayBuffer();

View file

@ -373,7 +373,7 @@
// 1. on an incoming message
// 2. on a sent message sync'd from another device
// 3. in rare cases, an incoming message can be retried, though it will
// still through one of the previous two codepaths.
// still go through one of the previous two codepaths
var message = this;
var source = message.get('source');
var type = message.get('type');

10
js/modules/.eslintrc Normal file
View file

@ -0,0 +1,10 @@
{
"env": {
"browser": false,
"commonjs": true,
"node": false
},
"globals": {
"console": true
}
}

View 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);
});
};

View file

@ -0,0 +1,185 @@
const isFunction = require('lodash/isFunction');
const isNumber = require('lodash/isNumber');
const isString = require('lodash/isString');
const isUndefined = require('lodash/isUndefined');
const MIME = require('./mime');
const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util');
const { autoOrientImage } = require('../auto_orient_image');
// Increment this version number every time we change how attachments are upgraded. This
// will allow 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:
exports.CURRENT_SCHEMA_VERSION = 2;
// Schema version history
//
// Version 1
// - Auto-orient JPEG attachments using EXIF `Orientation` data
// - Add `schemaVersion` property
// Version 2
// - Sanitize Unicode order override characters
// // 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
// }
// Returns true if `rawAttachment` is a valid attachment based on our (limited)
// criteria. Over time, we can expand this definition to become more narrow:
exports.isValid = (rawAttachment) => {
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
// deserialized by protobuf:
if (!rawAttachment) {
return false;
}
return isString(rawAttachment.contentType) &&
isString(rawAttachment.fileName);
};
// Middleware
// type UpgradeStep = Attachment -> Promise Attachment
// SchemaVersion -> UpgradeStep -> UpgradeStep
exports.withSchemaVersion = (schemaVersion, upgrade) => {
if (!isNumber(schemaVersion)) {
throw new TypeError('`schemaVersion` must be a number');
}
if (!isFunction(upgrade)) {
throw new TypeError('`upgrade` must be a function');
}
return async (attachment) => {
if (!exports.isValid(attachment)) {
console.log('Attachment.withSchemaVersion: Invalid input attachment:', attachment);
return attachment;
}
const isAlreadyUpgraded = attachment.schemaVersion >= schemaVersion;
if (isAlreadyUpgraded) {
return attachment;
}
const expectedVersion = schemaVersion - 1;
const isUnversioned = isUndefined(attachment.schemaVersion);
const hasExpectedVersion = isUnversioned ||
attachment.schemaVersion === expectedVersion;
if (!hasExpectedVersion) {
console.log(
'WARNING: Attachment.withSchemaVersion: Unexpected version:' +
` Expected attachment to have version ${expectedVersion},` +
` but got ${attachment.schemaVersion}.`,
attachment
);
return attachment;
}
let upgradedAttachment;
try {
upgradedAttachment = await upgrade(attachment);
} catch (error) {
console.log(
'Attachment.withSchemaVersion: error:',
error && error.stack ? error.stack : error
);
return attachment;
}
if (!exports.isValid(upgradedAttachment)) {
console.log(
'Attachment.withSchemaVersion: Invalid upgraded attachment:',
upgradedAttachment
);
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, wed preserve the original image data for users who want to
// retain it but due to reports of data loss, we dont 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;
};
const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D';
const UNICODE_RIGHT_TO_LEFT_OVERRIDE = '\u202E';
const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
const INVALID_CHARACTERS_PATTERN = new RegExp(
`[${UNICODE_LEFT_TO_RIGHT_OVERRIDE}${UNICODE_RIGHT_TO_LEFT_OVERRIDE}]`,
'g'
);
// NOTE: Expose synchronous version to do property-based testing using `testcheck`,
// which currently doesnt support async testing:
// https://github.com/leebyron/testcheck-js/issues/45
exports._replaceUnicodeOrderOverridesSync = (attachment) => {
if (!isString(attachment.fileName)) {
return attachment;
}
const normalizedFilename = attachment.fileName.replace(
INVALID_CHARACTERS_PATTERN,
UNICODE_REPLACEMENT_CHARACTER
);
const newAttachment = Object.assign({}, attachment, {
fileName: normalizedFilename,
});
return newAttachment;
};
exports.replaceUnicodeOrderOverrides = async attachment =>
exports._replaceUnicodeOrderOverridesSync(attachment);
// Public API
const toVersion1 = exports.withSchemaVersion(1, autoOrientJPEG);
const toVersion2 = exports.withSchemaVersion(2, exports.replaceUnicodeOrderOverrides);
// UpgradeStep
exports.upgradeSchema = async attachment =>
toVersion2(await toVersion1(attachment));

View 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
View file

@ -0,0 +1,2 @@
exports.isJPEG = mimeType =>
mimeType === 'image/jpeg';

View file

@ -78,13 +78,13 @@
if (ab1.byteLength !== ab2.byteLength) {
return false;
}
var result = true;
var result = 0;
var ta1 = new Uint8Array(ab1);
var ta2 = new Uint8Array(ab2);
for (var i = 0; i < ab1.byteLength; ++i) {
if (ta1[i] !== ta2[i]) { result = false; }
result = result | ta1[i] ^ ta2[i];
}
return result;
return result === 0;
}
var Model = Backbone.Model.extend({ database: Whisper.Database });
@ -688,7 +688,7 @@
}.bind(this));
},
// This matches the Java method as of
// https://github.com/WhisperSystems/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
// https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
processVerifiedMessage: function(identifier, verifiedStatus, publicKey) {
if (identifier === null || identifier === undefined) {
throw new Error("Tried to set verified for undefined/null key");

View file

@ -7,12 +7,12 @@
initialize: function(options) {
this.inboxView = null;
this.installView = null;
this.applyTheme();
this.applyHideMenu();
},
events: {
'click .openInstaller': 'openInstaller',
'click .openStandalone': 'openStandalone',
'click .openInstaller': 'openInstaller', // NetworkStatusView has this button
'openInbox': 'openInbox',
'change-theme': 'applyTheme',
'change-hide-menu': 'applyHideMenu',
@ -45,39 +45,38 @@
this.debugLogView = null;
}
},
openInstallChoice: function() {
this.closeInstallChoice();
var installChoice = this.installChoice = new Whisper.InstallChoiceView();
this.listenTo(installChoice, 'install-new', this.openInstaller.bind(this));
this.listenTo(installChoice, 'install-import', this.openImporter.bind(this));
this.openView(this.installChoice);
},
closeInstallChoice: function() {
if (this.installChoice) {
this.installChoice.remove();
this.installChoice = null;
}
},
openImporter: function() {
this.closeImporter();
this.closeInstallChoice();
window.addSetupMenuItems();
this.resetViews();
var importView = this.importView = new Whisper.ImportView();
this.listenTo(importView, 'cancel', this.openInstallChoice.bind(this));
this.listenTo(importView, 'light-import', this.finishLightImport.bind(this));
this.openView(this.importView);
},
finishLightImport: function() {
var options = {
hasExistingData: true
};
this.openInstaller(options);
},
closeImporter: function() {
if (this.importView) {
this.importView.remove();
this.importView = null;
}
},
openInstaller: function() {
this.closeInstaller();
this.closeInstallChoice();
var installView = this.installView = new Whisper.InstallView();
this.listenTo(installView, 'cancel', this.openInstallChoice.bind(this));
openInstaller: function(options) {
options = options || {};
// If we're in the middle of import, we don't want to show the menu options
// allowing the user to switch to other ways to set up the app. If they
// switched back and forth in the middle of a light import, they'd lose all
// that imported data.
if (!options.hasExistingData) {
window.addSetupMenuItems();
}
this.resetViews();
var installView = this.installView = new Whisper.InstallView(options);
this.openView(this.installView);
},
closeInstaller: function() {
@ -88,11 +87,23 @@
},
openStandalone: function() {
if (window.config.environment !== 'production') {
this.closeInstaller();
this.installView = new Whisper.StandaloneRegistrationView();
this.openView(this.installView);
window.addSetupMenuItems();
this.resetViews();
this.standaloneView = new Whisper.StandaloneRegistrationView();
this.openView(this.standaloneView);
}
},
closeStandalone: function() {
if (this.standaloneView) {
this.standaloneView.remove();
this.standaloneView = null;
}
},
resetViews: function() {
this.closeInstaller();
this.closeImporter();
this.closeStandalone();
},
openInbox: function(options) {
options = options || {};
// The inbox can be created before the 'empty' event fires or afterwards. If

View file

@ -1,271 +1,290 @@
/*
* vim: ts=4:sw=4:expandtab
*/
/* eslint-env browser */
/* global $: false */
/* global _: false */
/* global Backbone: false */
/* global moment: false */
/* global i18n: false */
/* global textsecure: false */
/* global Whisper: false */
// eslint-disable-next-line func-names
(function () {
'use strict';
const ESCAPE_KEY_CODE = 27;
var FileView = Whisper.View.extend({
tagName: 'div',
className: 'fileView',
templateName: 'file-view',
render_attributes: function() {
return this.model;
}
const FileView = Whisper.View.extend({
tagName: 'div',
className: 'fileView',
templateName: 'file-view',
render_attributes() {
return this.model;
},
});
var ImageView = Backbone.View.extend({
tagName: 'img',
initialize: function(dataUrl) {
this.dataUrl = dataUrl;
},
events: {
'load': 'update',
},
update: function() {
this.trigger('update');
},
render: function() {
this.$el.attr('src', this.dataUrl);
return this;
}
const ImageView = Backbone.View.extend({
tagName: 'img',
initialize(blobUrl) {
this.blobUrl = blobUrl;
},
events: {
load: 'update',
},
update() {
this.trigger('update');
},
render() {
this.$el.attr('src', this.blobUrl);
return this;
},
});
var MediaView = Backbone.View.extend({
initialize: function(dataUrl, contentType) {
this.dataUrl = dataUrl;
this.contentType = contentType;
this.$el.attr('controls', '');
},
events: {
'canplay': 'canplay'
},
canplay: function() {
this.trigger('update');
},
render: function() {
var $el = $('<source>');
$el.attr('src', this.dataUrl);
this.$el.append($el);
return this;
}
const MediaView = Backbone.View.extend({
initialize(dataUrl, { contentType } = {}) {
this.dataUrl = dataUrl;
this.contentType = contentType;
this.$el.attr('controls', '');
},
events: {
canplay: 'canplay',
},
canplay() {
this.trigger('update');
},
render() {
const $el = $('<source>');
$el.attr('src', this.dataUrl);
this.$el.append($el);
return this;
},
});
var AudioView = MediaView.extend({ tagName: 'audio' });
var VideoView = MediaView.extend({ tagName: 'video' });
const AudioView = MediaView.extend({ tagName: 'audio' });
const VideoView = MediaView.extend({ tagName: 'video' });
// Blacklist common file types known to be unsupported in Chrome
var UnsupportedFileTypes = [
const UnsupportedFileTypes = [
'audio/aiff',
'video/quicktime'
'video/quicktime',
];
Whisper.AttachmentView = Backbone.View.extend({
tagName: 'span',
className: function() {
className() {
if (this.isImage()) {
return 'attachment';
} else {
return 'attachment bubbled';
}
return 'attachment bubbled';
},
initialize(options) {
this.blob = new Blob([this.model.data], { type: this.model.contentType });
if (!this.model.size) {
this.model.size = this.model.data.byteLength;
}
if (options.timestamp) {
this.timestamp = options.timestamp;
}
},
initialize: function(options) {
this.blob = new Blob([this.model.data], {type: this.model.contentType});
if (!this.model.size) {
this.model.size = this.model.data.byteLength;
}
if (options.timestamp) {
this.timestamp = options.timestamp;
}
},
events: {
'click': 'onclick'
click: 'onclick',
},
unload: function() {
this.blob = null;
unload() {
this.blob = null;
if (this.lightBoxView) {
this.lightBoxView.remove();
}
if (this.fileView) {
this.fileView.remove();
}
if (this.view) {
this.view.remove();
}
if (this.lightBoxView) {
this.lightBoxView.remove();
}
if (this.fileView) {
this.fileView.remove();
}
if (this.view) {
this.view.remove();
}
this.remove();
this.remove();
},
getFileType: function() {
switch(this.model.contentType) {
case 'video/quicktime': return 'mov';
default: return this.model.contentType.split('/')[1];
}
getFileType() {
switch (this.model.contentType) {
case 'video/quicktime': return 'mov';
default: return this.model.contentType.split('/')[1];
}
},
onclick: function(e) {
if (this.isImage()) {
this.lightBoxView = new Whisper.LightboxView({ model: this });
this.lightBoxView.render();
this.lightBoxView.$el.appendTo(this.el);
this.lightBoxView.$el.trigger('show');
onclick() {
if (this.isImage()) {
this.lightBoxView = new Whisper.LightboxView({ model: this });
this.lightBoxView.render();
this.lightBoxView.$el.appendTo(this.el);
this.lightBoxView.$el.trigger('show');
} else {
this.saveFile();
}
},
isVoiceMessage() {
// eslint-disable-next-line no-bitwise
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) {
return true;
}
} else {
this.saveFile();
}
},
isVoiceMessage: function() {
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) {
return true;
}
// Support for android legacy voice messages
if (this.isAudio() && this.model.fileName === null) {
return true;
}
// Support for android legacy voice messages
if (this.isAudio() && this.model.fileName === null) {
return true;
}
return false;
},
isAudio: function() {
return this.model.contentType.startsWith('audio/');
isAudio() {
return this.model.contentType.startsWith('audio/');
},
isVideo: function() {
return this.model.contentType.startsWith('video/');
isVideo() {
return this.model.contentType.startsWith('video/');
},
isImage: function() {
var type = this.model.contentType;
return type.startsWith('image/') && type !== 'image/tiff';
isImage() {
const type = this.model.contentType;
return type.startsWith('image/') && type !== 'image/tiff';
},
mediaType: function() {
if (this.isVoiceMessage()) {
return 'voice';
} else if (this.isAudio()) {
return 'audio';
} else if (this.isVideo()) {
return 'video';
} else if (this.isImage()) {
return 'image';
}
},
displayName: function() {
if (this.isVoiceMessage()) {
return i18n('voiceMessage');
}
if (this.model.fileName) {
return this.model.fileName;
}
if (this.isAudio() || this.isVideo()) {
return i18n('mediaMessage');
}
mediaType() {
if (this.isVoiceMessage()) {
return 'voice';
} else if (this.isAudio()) {
return 'audio';
} else if (this.isVideo()) {
return 'video';
} else if (this.isImage()) {
return 'image';
}
return i18n('unnamedFile');
// NOTE: The existing code had no `return` but ESLint insists. Thought
// about throwing an error assuming this was unreachable code but it turns
// out that content type `image/tiff` falls through here:
return undefined;
},
suggestedName: function() {
if (this.model.fileName) {
return this.model.fileName;
}
displayName() {
if (this.isVoiceMessage()) {
return i18n('voiceMessage');
}
if (this.model.fileName) {
return this.model.fileName;
}
if (this.isAudio() || this.isVideo()) {
return i18n('mediaMessage');
}
var suggestion = 'signal';
if (this.timestamp) {
suggestion += moment(this.timestamp).format('-YYYY-MM-DD-HHmmss');
}
var fileType = this.getFileType();
if (fileType) {
suggestion += '.' + fileType;
}
return suggestion;
return i18n('unnamedFile');
},
saveFile: function() {
var url = window.URL.createObjectURL(this.blob, { type: 'octet/stream' });
var a = $('<a>').attr({ href: url, download: this.suggestedName() });
a[0].click();
window.URL.revokeObjectURL(url);
},
render: function() {
if (!this.isImage()) {
this.renderFileView();
}
var View;
if (this.isImage()) {
View = ImageView;
} else if (this.isAudio()) {
View = AudioView;
} else if (this.isVideo()) {
View = VideoView;
}
suggestedName() {
if (this.model.fileName) {
return this.model.fileName;
}
if (!View || _.contains(UnsupportedFileTypes, this.model.contentType)) {
this.update();
return this;
}
if (!this.objectUrl) {
this.objectUrl = window.URL.createObjectURL(this.blob);
}
this.view = new View(this.objectUrl, this.model.contentType);
this.view.$el.appendTo(this.$el);
this.listenTo(this.view, 'update', this.update);
this.view.render();
if (View !== ImageView) {
this.timeout = setTimeout(this.onTimeout.bind(this), 5000);
}
return this;
let suggestion = 'signal';
if (this.timestamp) {
suggestion += moment(this.timestamp).format('-YYYY-MM-DD-HHmmss');
}
const fileType = this.getFileType();
if (fileType) {
suggestion += `.${fileType}`;
}
return suggestion;
},
onTimeout: function() {
// Image or media element failed to load. Fall back to FileView.
this.stopListening(this.view);
saveFile() {
const url = window.URL.createObjectURL(this.blob, { type: 'octet/stream' });
const a = $('<a>').attr({ href: url, download: this.suggestedName() });
a[0].click();
window.URL.revokeObjectURL(url);
},
render() {
if (!this.isImage()) {
this.renderFileView();
}
let View;
if (this.isImage()) {
View = ImageView;
} else if (this.isAudio()) {
View = AudioView;
} else if (this.isVideo()) {
View = VideoView;
}
if (!View || _.contains(UnsupportedFileTypes, this.model.contentType)) {
this.update();
},
renderFileView: function() {
this.fileView = new FileView({
model: {
mediaType: this.mediaType(),
fileName: this.displayName(),
fileSize: window.filesize(this.model.size),
altText: i18n('clickToSave')
}
});
this.fileView.$el.appendTo(this.$el.empty());
this.fileView.render();
return this;
}
if (!this.objectUrl) {
this.objectUrl = window.URL.createObjectURL(this.blob);
}
const { blob } = this;
const { contentType } = this.model;
this.view = new View(this.objectUrl, { blob, contentType });
this.view.$el.appendTo(this.$el);
this.listenTo(this.view, 'update', this.update);
this.view.render();
if (View !== ImageView) {
this.timeout = setTimeout(this.onTimeout.bind(this), 5000);
}
return this;
},
onTimeout() {
// Image or media element failed to load. Fall back to FileView.
this.stopListening(this.view);
this.update();
},
renderFileView() {
this.fileView = new FileView({
model: {
mediaType: this.mediaType(),
fileName: this.displayName(),
fileSize: window.filesize(this.model.size),
altText: i18n('clickToSave'),
},
});
this.fileView.$el.appendTo(this.$el.empty());
this.fileView.render();
return this;
},
update() {
clearTimeout(this.timeout);
this.trigger('update');
},
update: function() {
clearTimeout(this.timeout);
this.trigger('update');
}
});
Whisper.LightboxView = Whisper.View.extend({
templateName: 'lightbox',
className: 'modal lightbox',
initialize: function() {
this.window = window;
this.$document = $(this.window.document);
this.listener = this.onkeyup.bind(this);
this.$document.on('keyup', this.listener);
},
events: {
'click .save': 'save',
'click .close': 'remove',
'click': 'onclick'
},
save: function(e) {
this.model.saveFile();
},
onclick: function(e) {
var $el = this.$(e.target);
if (!$el.hasClass('image') && !$el.closest('.controls').length ) {
e.preventDefault();
this.remove();
return false;
}
},
onkeyup: function(e) {
if (e.keyCode === 27) {
this.remove();
this.$document.off('keyup', this.listener);
}
},
render_attributes: function() {
return { url: this.model.objectUrl };
templateName: 'lightbox',
className: 'modal lightbox',
initialize() {
this.window = window;
this.$document = $(this.window.document);
this.listener = this.onkeyup.bind(this);
this.$document.on('keyup', this.listener);
},
events: {
'click .save': 'save',
'click .close': 'remove',
click: 'onclick',
},
save() {
this.model.saveFile();
},
onclick(e) {
const $el = this.$(e.target);
if (!$el.hasClass('image') && !$el.closest('.controls').length) {
e.preventDefault();
this.remove();
return false;
}
});
})();
return true;
},
onkeyup(e) {
if (e.keyCode === ESCAPE_KEY_CODE) {
this.remove();
this.$document.off('keyup', this.listener);
}
},
render_attributes() {
return { url: this.model.objectUrl };
},
});
}());

View file

@ -1,10 +1,13 @@
/*
* vim: ts=4:sw=4:expandtab
*/
/* eslint-disable */
/* global textsecure: false */
(function () {
'use strict';
window.Whisper = window.Whisper || {};
const { MIME } = window.Signal.Types;
Whisper.FileSizeToast = Whisper.ToastView.extend({
templateName: 'file-size-modal',
render_attributes: function() {
@ -30,6 +33,7 @@
this.thumb = new Whisper.AttachmentPreviewView();
this.$el.addClass('file-input');
this.window = options.window;
this.previewObjectUrl = null;
},
events: {
@ -93,7 +97,6 @@
return;
}
// loadImage.scale -> components/blueimp-load-image
var canvas = loadImage.scale(img, {
canvas: true, maxWidth: maxWidth, maxHeight: maxHeight
});
@ -103,11 +106,13 @@
var blob;
do {
i = i - 1;
// dataURLtoBlob -> components/blueimp-canvas-to-blob
blob = dataURLtoBlob(
blob = window.dataURLToBlobSync(
canvas.toDataURL('image/jpeg', quality)
);
quality = quality * maxSize / blob.size;
// NOTE: During testing with a large image, we observed the
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
if (quality < 0.5) {
quality = 0.5;
}
@ -132,13 +137,25 @@
case 'audio': this.addThumb('images/audio.svg'); break;
case 'video': this.addThumb('images/video.svg'); break;
case 'image':
this.oUrl = URL.createObjectURL(file);
this.addThumb(this.oUrl);
if (!MIME.isJPEG(file.type)) {
this.previewObjectUrl = URL.createObjectURL(file);
this.addThumb(this.previewObjectUrl);
break;
}
// NOTE: Temporarily allow `then` until we convert the entire file
// to `async` / `await`:
// eslint-disable-next-line more/no-then
window.autoOrientImage(file)
.then(dataURL => this.addThumb(dataURL));
break;
default:
this.addThumb('images/file.svg'); break;
}
// NOTE: Temporarily allow `then` until we convert the entire file
// to `async` / `await`:
// eslint-disable-next-line more/no-then
this.autoScale(file).then(function(blob) {
var limitKb = 1000000;
var blobType = file.type === 'image/gif' ? 'gif' : type;
@ -177,30 +194,41 @@
return files && files.length && files.length > 0;
},
getFiles: function() {
var promises = [];
var files = this.file ? [this.file] : this.$input.prop('files');
for (var i = 0; i < files.length; i++) {
promises.push(this.getFile(files[i]));
}
this.clearForm();
return Promise.all(promises);
},
/* eslint-enable */
/* jshint ignore:start */
getFiles() {
const files = this.file ? [this.file] : Array.from(this.$input.prop('files'));
const promise = Promise.all(files.map(file => this.getFile(file)));
this.clearForm();
return promise;
},
getFile: function(file) {
file = file || this.file || this.$input.prop('files')[0];
if (file === undefined) { return Promise.resolve(); }
var flags;
if (this.isVoiceNote) {
flags = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
}
return this.autoScale(file).then(this.readFile).then(function(attachment) {
if (flags) {
attachment.flags = flags;
}
return attachment;
}.bind(this));
},
getFile(rawFile) {
const file = rawFile || this.file || this.$input.prop('files')[0];
if (file === undefined) {
return Promise.resolve();
}
const attachmentFlags = this.isVoiceNote
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
: null;
const setFlags = flags => (attachment) => {
const newAttachment = Object.assign({}, attachment);
if (flags) {
newAttachment.flags = flags;
}
return newAttachment;
};
// NOTE: Temporarily allow `then` until we convert the entire file
// to `async` / `await`:
// eslint-disable-next-line more/no-then
return this.autoScale(file)
.then(this.readFile)
.then(setFlags(attachmentFlags));
},
/* jshint ignore:end */
/* eslint-disable */
getThumbnail: function() {
// Scale and crop an image to 256px square
@ -228,8 +256,7 @@
crop: true, minWidth: size, minHeight: size
});
// dataURLtoBlob -> components/blueimp-canvas-to-blob
var blob = dataURLtoBlob(canvas.toDataURL('image/png'));
var blob = window.dataURLToBlobSync(canvas.toDataURL('image/png'));
resolve(blob);
};
@ -237,6 +264,7 @@
}).then(this.readFile);
},
// File -> Promise Attachment
readFile: function(file) {
return new Promise(function(resolve, reject) {
var FR = new FileReader();
@ -255,10 +283,11 @@
},
clearForm: function() {
if (this.oUrl) {
URL.revokeObjectURL(this.oUrl);
this.oUrl = null;
if (this.previewObjectUrl) {
URL.revokeObjectURL(this.previewObjectUrl);
this.previewObjectUrl = null;
}
this.thumb.remove();
this.$('.avatar').show();
this.$el.trigger('force-resize');

View file

@ -7,7 +7,8 @@
var State = {
IMPORTING: 1,
COMPLETE: 2
COMPLETE: 2,
LIGHT_COMPLETE: 3,
};
var IMPORT_STARTED = 'importStarted';
@ -39,12 +40,13 @@
};
Whisper.ImportView = Whisper.View.extend({
templateName: 'app-migration-screen',
className: 'app-loading-screen',
templateName: 'import-flow-template',
className: 'full-screen-flow',
events: {
'click .import': 'onImport',
'click .choose': 'onImport',
'click .restart': 'onRestart',
'click .cancel': 'onCancel',
'click .register': 'onRegister',
},
initialize: function() {
if (Whisper.Import.isIncomplete()) {
@ -55,41 +57,42 @@
this.pending = Promise.resolve();
},
render_attributes: function() {
var message;
var importButton;
var hideProgress = true;
var restartButton;
var cancelButton;
if (this.error) {
return {
message: i18n('importError'),
hideProgress: true,
importButton: i18n('tryAgain'),
isError: true,
errorHeader: i18n('importErrorHeader'),
errorMessage: i18n('importError'),
chooseButton: i18n('importAgain'),
};
}
switch (this.state) {
case State.COMPLETE:
message = i18n('importComplete');
restartButton = i18n('restartSignal');
break;
case State.IMPORTING:
message = i18n('importing');
hideProgress = false;
break;
default:
message = i18n('importInstructions');
importButton = i18n('chooseDirectory');
cancelButton = i18n('cancel');
var restartButton = i18n('importCompleteStartButton');
var registerButton = i18n('importCompleteLinkButton');
var step = 'step2';
if (this.state === State.IMPORTING) {
step = 'step3';
} else if (this.state === State.COMPLETE) {
registerButton = null;
step = 'step4';
} else if (this.state === State.LIGHT_COMPLETE) {
restartButton = null;
step = 'step4';
}
return {
hideProgress: hideProgress,
message: message,
importButton: importButton,
isStep2: step === 'step2',
chooseHeader: i18n('loadDataHeader'),
choose: i18n('loadDataDescription'),
chooseButton: i18n('chooseDirectory'),
isStep3: step === 'step3',
importingHeader: i18n('importingHeader'),
isStep4: step === 'step4',
completeHeader: i18n('importCompleteHeader'),
restartButton: restartButton,
cancelButton: cancelButton,
registerButton: registerButton,
};
},
onRestart: function() {
@ -110,9 +113,16 @@
}
});
},
doImport: function(directory) {
this.error = null;
onRegister: function() {
// AppView listens for this, and opens up InstallView to the QR code step to
// finish setting this device up.
this.trigger('light-import');
},
doImport: function(directory) {
window.removeSetupMenuItems();
this.error = null;
this.state = State.IMPORTING;
this.render();
@ -125,25 +135,17 @@
Whisper.Import.start(),
Whisper.Backup.importFromDirectory(directory)
]);
}).then(function() {
// Catching in-memory cache up with what's in indexeddb now...
// NOTE: this fires storage.onready, listened to across the app. We'll restart
// to complete the install to start up cleanly with everything now in the DB.
return storage.fetch();
}).then(function() {
return Promise.all([
// Clearing any migration-related state inherited from the Chrome App
storage.remove('migrationState'),
storage.remove('migrationEnabled'),
storage.remove('migrationEverCompleted'),
storage.remove('migrationStorageLocation'),
}).then(function(results) {
var importResult = results[1];
Whisper.Import.saveLocation(directory),
Whisper.Import.complete()
]);
}).then(function() {
this.state = State.COMPLETE;
this.render();
// A full import changes so much we need a restart of the app
if (importResult.fullImport) {
return this.finishFullImport(directory);
}
// A light import just brings in contacts, groups, and messages. And we need a
// normal link to finish the process.
return this.finishLightImport(directory);
}.bind(this)).catch(function(error) {
console.log('Error importing:', error && error.stack ? error.stack : error);
@ -153,6 +155,40 @@
return Whisper.Backup.clearDatabase();
}.bind(this));
},
finishLightImport: function(directory) {
ConversationController.reset();
return ConversationController.load().then(function() {
return Promise.all([
Whisper.Import.saveLocation(directory),
Whisper.Import.complete(),
]);
}).then(function() {
this.state = State.LIGHT_COMPLETE;
this.render();
}.bind(this));
},
finishFullImport: function(directory) {
// Catching in-memory cache up with what's in indexeddb now...
// NOTE: this fires storage.onready, listened to across the app. We'll restart
// to complete the install to start up cleanly with everything now in the DB.
return storage.fetch()
.then(function() {
return Promise.all([
// Clearing any migration-related state inherited from the Chrome App
storage.remove('migrationState'),
storage.remove('migrationEnabled'),
storage.remove('migrationEverCompleted'),
storage.remove('migrationStorageLocation'),
Whisper.Import.saveLocation(directory),
Whisper.Import.complete()
]);
}).then(function() {
this.state = State.COMPLETE;
this.render();
}.bind(this));
}
});
})();

View file

@ -1,31 +0,0 @@
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.InstallChoiceView = Whisper.View.extend({
templateName: 'install-choice',
className: 'install install-choice',
events: {
'click .new': 'onClickNew',
'click .import': 'onClickImport'
},
initialize: function() {
this.render();
},
render_attributes: {
installWelcome: i18n('installWelcome'),
installTagline: i18n('installTagline'),
installNew: i18n('installNew'),
installImport: i18n('installImport')
},
onClickNew: function() {
this.trigger('install-new');
},
onClickImport: function() {
this.trigger('install-import');
}
});
})();

View file

@ -14,145 +14,176 @@
NETWORK_ERROR: 'NetworkError',
};
var DEVICE_NAME_SELECTOR = 'input.device-name';
var CONNECTION_ERROR = -1;
var TOO_MANY_DEVICES = 411;
Whisper.InstallView = Whisper.View.extend({
templateName: 'install_flow_template',
className: 'main install',
render_attributes: function() {
var twitterHref = 'https://twitter.com/whispersystems';
var signalHref = 'https://signal.org/install';
return {
installWelcome: i18n('installWelcome'),
installTagline: i18n('installTagline'),
installGetStartedButton: i18n('installGetStartedButton'),
installSignalLink: this.i18n_with_links('installSignalLink', signalHref),
installIHaveSignalButton: i18n('installGotIt'),
installFollowUs: this.i18n_with_links('installFollowUs', twitterHref),
installAndroidInstructions: i18n('installAndroidInstructions'),
installLinkingWithNumber: i18n('installLinkingWithNumber'),
installComputerName: i18n('installComputerName'),
installFinalButton: i18n('installFinalButton'),
installTooManyDevices: i18n('installTooManyDevices'),
installConnectionFailed: i18n('installConnectionFailed'),
ok: i18n('ok'),
tryAgain: i18n('tryAgain'),
development: window.config.environment === 'development'
};
templateName: 'link-flow-template',
className: 'main full-screen-flow',
events: {
'click .try-again': 'connect',
// handler for finish button is in confirmNumber()
},
initialize: function(options) {
this.counter = 0;
options = options || {};
this.render();
var deviceName = textsecure.storage.user.getDeviceName();
if (!deviceName) {
deviceName = window.config.hostname;
}
this.$('#device-name').val(deviceName);
this.selectStep(Steps.INSTALL_SIGNAL);
this.selectStep(Steps.SCAN_QR_CODE);
this.connect();
this.on('disconnected', this.reconnect);
if (Whisper.Registration.everDone()) {
this.selectStep(Steps.SCAN_QR_CODE);
this.hideDots();
// Keep data around if it's a re-link, or the middle of a light import
this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData;
},
render_attributes: function() {
var errorMessage;
if (this.error) {
if (this.error.name === 'HTTPError'
&& this.error.code == TOO_MANY_DEVICES) {
errorMessage = i18n('installTooManyDevices');
}
else if (this.error.name === 'HTTPError'
&& this.error.code == CONNECTION_ERROR) {
errorMessage = i18n('installConnectionFailed');
}
else if (this.error.message === 'websocket closed') {
// AccountManager.registerSecondDevice uses this specific
// 'websocket closed' error message
errorMessage = i18n('installConnectionFailed');
}
return {
isError: true,
errorHeader: 'Something went wrong!',
errorMessage,
errorButton: 'Try again',
};
}
return {
isStep3: this.step === Steps.SCAN_QR_CODE,
linkYourPhone: i18n('linkYourPhone'),
signalSettings: i18n('signalSettings'),
linkedDevices: i18n('linkedDevices'),
androidFinalStep: i18n('plusButton'),
appleFinalStep: i18n('linkNewDevice'),
isStep4: this.step === Steps.ENTER_NAME,
chooseName: i18n('chooseDeviceName'),
finishLinkingPhoneButton: i18n('finishLinkingPhone'),
isStep5: this.step === Steps.PROGRESS_BAR,
syncing: i18n('initialSync'),
};
},
selectStep: function(step) {
this.step = step;
this.render();
},
connect: function() {
this.error = null;
this.selectStep(Steps.SCAN_QR_CODE);
this.clearQR();
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
var accountManager = getAccountManager();
accountManager.registerSecondDevice(
this.setProvisioningUrl.bind(this),
this.confirmNumber.bind(this),
this.incrementCounter.bind(this)
this.confirmNumber.bind(this)
).catch(this.handleDisconnect.bind(this));
},
handleDisconnect: function(e) {
if (this.canceled) {
return;
}
console.log('provisioning failed', e.stack);
this.error = e;
this.render();
if (e.message === 'websocket closed') {
this.showConnectionError();
this.trigger('disconnected');
} else if (e.name === 'HTTPError' && e.code == -1) {
this.selectStep(Steps.NETWORK_ERROR);
} else if (e.name === 'HTTPError' && e.code == 411) {
this.showTooManyDevices();
} else {
} else if (e.name !== 'HTTPError'
|| (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) {
throw e;
}
},
reconnect: function() {
setTimeout(this.connect.bind(this), 10000);
},
events: function() {
return {
'click .error-dialog .ok': 'connect',
'click .step1': 'onCancel',
'click .step2': this.selectStep.bind(this, Steps.INSTALL_SIGNAL),
'click .step3': this.selectStep.bind(this, Steps.SCAN_QR_CODE)
};
},
onCancel: function() {
this.canceled = true;
this.trigger('cancel');
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.timeout = setTimeout(this.connect.bind(this), 10000);
},
clearQR: function() {
this.$('#qr').text(i18n("installConnecting"));
this.$('#qr img').remove();
this.$('#qr canvas').remove();
this.$('#qr .container').show();
this.$('#qr').removeClass('ready');
},
setProvisioningUrl: function(url) {
this.$('#qr').html('');
new QRCode(this.$('#qr')[0]).makeCode(url);
if ($('#qr').length === 0) {
console.log('Did not find #qr element in the DOM!');
return;
}
this.$('#qr .container').hide();
this.qr = new QRCode(this.$('#qr')[0]).makeCode(url);
this.$('#qr').removeAttr('title');
this.$('#qr').addClass('ready');
},
setDeviceNameDefault: function() {
var deviceName = textsecure.storage.user.getDeviceName();
this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.config.hostname);
this.$(DEVICE_NAME_SELECTOR).focus();
},
confirmNumber: function(number) {
var parsed = libphonenumber.parse(number);
var stepId = '#step' + Steps.ENTER_NAME;
this.$(stepId + ' .number').text(libphonenumber.format(
parsed,
libphonenumber.PhoneNumberFormat.INTERNATIONAL
));
window.removeSetupMenuItems();
this.selectStep(Steps.ENTER_NAME);
this.$('#device-name').focus();
this.setDeviceNameDefault();
return new Promise(function(resolve, reject) {
this.$(stepId + ' .cancel').click(function(e) {
reject();
});
this.$(stepId).submit(function(e) {
this.$('.finish').click(function(e) {
e.stopPropagation();
e.preventDefault();
var name = this.$('#device-name').val();
var name = this.$(DEVICE_NAME_SELECTOR).val();
name = name.replace(/\0/g,''); // strip unicode null
if (name.trim().length === 0) {
this.$('#device-name').focus();
this.$(DEVICE_NAME_SELECTOR).focus();
return;
}
this.$('.progress-dialog .status').text(i18n('installGeneratingKeys'));
this.selectStep(Steps.PROGRESS_BAR);
resolve(name);
var finish = function() {
resolve(name);
};
// Delete all data from database unless we're in the middle
// of a re-link, or we are finishing a light import. Without this,
// app restarts at certain times can cause weird things to happen,
// like data from a previous incomplete light import showing up
// after a new install.
if (this.shouldRetainData) {
return finish();
}
Whisper.Backup.clearDatabase().then(finish, function(error) {
console.log(
'confirmNumber: error clearing database',
error && error.stack ? error.stack : error
);
finish();
});
}.bind(this));
}.bind(this));
},
incrementCounter: function() {
this.$('.progress-dialog .bar').css('width', (++this.counter * 100 / 100) + '%');
},
selectStep: function(step) {
this.$('.step').hide();
this.$('#step' + step).show();
},
showSync: function() {
this.$('.progress-dialog .status').text(i18n('installSyncingGroupsAndContacts'));
this.$('.progress-dialog .bar').addClass('progress-bar-striped active');
},
showTooManyDevices: function() {
this.selectStep(Steps.TOO_MANY_DEVICES);
},
showConnectionError: function() {
this.$('#qr').text(i18n("installConnectionFailed"));
},
hideDots: function() {
this.$('#step' + Steps.SCAN_QR_CODE + ' .nav .dot').hide();
}
});
})();

View file

@ -82,13 +82,19 @@
render_attributes: function() {
var seconds = this.model.get('expirationTimerUpdate').expireTimer;
var timerMessage;
if (this.conversation.id === textsecure.storage.user.getNumber()) {
timerMessage = i18n('youChangedTheTimer',
Whisper.ExpirationTimerOptions.getName(seconds));
var timerUpdate = this.model.get('expirationTimerUpdate');
var prettySeconds = Whisper.ExpirationTimerOptions.getName(seconds);
if (timerUpdate && timerUpdate.fromSync) {
timerMessage = i18n('timerSetOnSync', prettySeconds);
} else if (this.conversation.id === textsecure.storage.user.getNumber()) {
timerMessage = i18n('youChangedTheTimer', prettySeconds);
} else {
timerMessage = i18n('theyChangedTheTimer', [
this.conversation.getTitle(),
Whisper.ExpirationTimerOptions.getName(seconds)]);
prettySeconds,
]);
}
return { content: timerMessage };
}

View file

@ -55,6 +55,7 @@
className: 'settings modal expand',
templateName: 'settings',
initialize: function() {
this.deviceName = textsecure.storage.user.getDeviceName();
this.render();
new RadioButtonGroupView({
el: this.$('.notification-settings'),
@ -88,6 +89,8 @@
},
render_attributes: function() {
return {
deviceNameLabel: i18n('deviceName'),
deviceName: this.deviceName,
theme: i18n('theme'),
notifications: i18n('notifications'),
notificationSettingsDialog: i18n('notificationSettingsDialog'),

View file

@ -7,7 +7,7 @@
Whisper.StandaloneRegistrationView = Whisper.View.extend({
templateName: 'standalone',
className: 'install main',
className: 'full-screen-flow',
initialize: function() {
this.accountManager = getAccountManager();
@ -21,16 +21,15 @@
this.$('#error').hide();
},
events: {
'submit #form': 'submit',
'validation input.number': 'onValidation',
'change #code': 'onChangeCode',
'click #request-voice': 'requestVoice',
'click #request-sms': 'requestSMSVerification',
'change #code': 'onChangeCode',
'click #verifyCode': 'verifyCode',
},
submit: function(e) {
e.preventDefault();
verifyCode: function(e) {
var number = this.phoneView.validateNumber();
var verificationCode = $('#code').val().replace(/\D+/g, "");
var verificationCode = $('#code').val().replace(/\D+/g, '');
this.accountManager.registerSingleDevice(number, verificationCode).then(function() {
this.$el.trigger('openInbox');
@ -64,6 +63,7 @@
}
},
requestVoice: function() {
window.removeSetupMenuItems();
this.$('#error').hide();
var number = this.phoneView.validateNumber();
if (number) {
@ -74,6 +74,7 @@
}
},
requestSMSVerification: function() {
window.removeSetupMenuItems();
$('#error').hide();
var number = this.phoneView.validateNumber();
if (number) {

View file

@ -35291,8 +35291,6 @@ var Internal = Internal || {};
result = result | (a[i] ^ b[i]);
}
if (result !== 0) {
console.log('Our MAC ', dcodeIO.ByteBuffer.wrap(calculated_mac).toHex());
console.log('Their MAC', dcodeIO.ByteBuffer.wrap(mac).toHex());
throw new Error("Bad MAC");
}
});
@ -36016,14 +36014,7 @@ libsignal.SessionBuilder = function (storage, remoteAddress) {
this.processV3 = builder.processV3.bind(builder);
};
function SessionCipher(storage, remoteAddress, options) {
options = options || {};
if (typeof options.messageKeysLimit === 'undefined') {
options.messageKeysLimit = 1000;
}
this.messageKeysLimit = options.messageKeysLimit;
function SessionCipher(storage, remoteAddress) {
this.remoteAddress = remoteAddress;
this.storage = storage;
}
@ -36296,15 +36287,14 @@ SessionCipher.prototype = {
});
},
fillMessageKeys: function(chain, counter) {
if (this.messageKeysLimit && Object.keys(chain.messageKeys).length >= this.messageKeysLimit) {
console.log("Too many message keys for chain");
return Promise.resolve(); // Stalker, much?
}
if (chain.chainKey.counter >= counter) {
return Promise.resolve(); // Already calculated
}
if (counter - chain.chainKey.counter > 2000) {
throw new Error('Over 2000 messages into the future!');
}
if (chain.chainKey.key === undefined) {
throw new Error("Got invalid request to extend chain after it was already closed");
}
@ -36433,8 +36423,8 @@ SessionCipher.prototype = {
}
};
libsignal.SessionCipher = function(storage, remoteAddress, options) {
var cipher = new SessionCipher(storage, remoteAddress, options);
libsignal.SessionCipher = function(storage, remoteAddress) {
var cipher = new SessionCipher(storage, remoteAddress);
// returns a Promise that resolves to a ciphertext object
this.encrypt = cipher.encrypt.bind(cipher);

View file

@ -558,23 +558,23 @@ MessageReceiver.prototype.extend({
handleReceiptMessage: function(envelope, receiptMessage) {
var results = [];
if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY) {
for (var i = 0; i < receiptMessage.timestamps.length; ++i) {
for (var i = 0; i < receiptMessage.timestamp.length; ++i) {
var ev = new Event('delivery');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.deliveryReceipt = {
timestamp : receiptMessage.timestamps[i].toNumber(),
timestamp : receiptMessage.timestamp[i].toNumber(),
source : envelope.source,
sourceDevice : envelope.sourceDevice
};
results.push(this.dispatchAndWait(ev));
}
} else if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ) {
for (var i = 0; i < receiptMessage.timestamps.length; ++i) {
for (var i = 0; i < receiptMessage.timestamp.length; ++i) {
var ev = new Event('read');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.timestamp = envelope.timestamp.toNumber();
ev.read = {
timestamp : receiptMessage.timestamps[i].toNumber(),
timestamp : receiptMessage.timestamp[i].toNumber(),
reader : envelope.source
}
results.push(this.dispatchAndWait(ev));
@ -626,17 +626,17 @@ MessageReceiver.prototype.extend({
return this.handleRead(envelope, syncMessage.read);
} else if (syncMessage.verified) {
return this.handleVerified(envelope, syncMessage.verified);
} else if (syncMessage.settings) {
return this.handleSettings(envelope, syncMessage.settings);
} else if (syncMessage.configuration) {
return this.handleConfiguration(envelope, syncMessage.configuration);
} else {
throw new Error('Got empty SyncMessage');
}
},
handleSettings: function(envelope, settings) {
var ev = new Event('settings');
handleConfiguration: function(envelope, configuration) {
var ev = new Event('configuration');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.settings = {
readReceipts: settings.readReceipts
ev.configuration = {
readReceipts: configuration.readReceipts
};
return this.dispatchAndWait(ev);
},
@ -815,8 +815,8 @@ MessageReceiver.prototype.extend({
// Before June, all incoming messages were still DataMessage:
// - iOS: Michael Kirk says that they were sending Legacy messages until June
// - Desktop: https://github.com/WhisperSystems/Signal-Desktop/commit/e8548879db405d9bcd78b82a456ad8d655592c0f
// - Android: https://github.com/WhisperSystems/libsignal-service-java/commit/61a75d023fba950ff9b4c75a249d1a3408e12958
// - Desktop: https://github.com/signalapp/Signal-Desktop/commit/e8548879db405d9bcd78b82a456ad8d655592c0f
// - Android: https://github.com/signalapp/libsignal-service-java/commit/61a75d023fba950ff9b4c75a249d1a3408e12958
//
// var d = new Date('2017-06-01T07:00:00.000Z');
// d.getTime();

View file

@ -11,7 +11,7 @@
console.log(text);
throw error;
}
var protos = result.build('textsecure');
var protos = result.build('signalservice');
if (!protos) {
var text = 'Error loading protos from ' + filename + ' (root: ' + window.PROTO_ROOT + ')';
console.log(text);
@ -23,7 +23,7 @@
});
};
loadProtoBufs('IncomingPushMessageSignal.proto');
loadProtoBufs('SignalService.proto');
loadProtoBufs('SubProtocol.proto');
loadProtoBufs('DeviceMessages.proto');
})();

View file

@ -388,7 +388,7 @@ MessageSender.prototype = {
sendReadReceipts: function(sender, timestamps) {
var receiptMessage = new textsecure.protobuf.ReceiptMessage();
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
receiptMessage.timestamps = timestamps;
receiptMessage.timestamp = timestamps;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.receiptMessage = receiptMessage;

View file

@ -17,7 +17,7 @@ describe("ContactBuffer", function() {
var contactInfo = new textsecure.protobuf.ContactDetails({
name: "Zero Cool",
number: "+10000000000",
avatar: { contentType: "image/jpg", length: avatarLen }
avatar: { contentType: "image/jpeg", length: avatarLen }
});
var contactInfoBuffer = contactInfo.encode().toArrayBuffer();
@ -41,7 +41,7 @@ describe("ContactBuffer", function() {
count++;
assert.strictEqual(contact.name, "Zero Cool");
assert.strictEqual(contact.number, "+10000000000");
assert.strictEqual(contact.avatar.contentType, "image/jpg");
assert.strictEqual(contact.avatar.contentType, "image/jpeg");
assert.strictEqual(contact.avatar.length, 255);
assert.strictEqual(contact.avatar.data.byteLength, 255);
var avatarBytes = new Uint8Array(contact.avatar.data);
@ -68,7 +68,7 @@ describe("GroupBuffer", function() {
id: new Uint8Array([1, 3, 3, 7]).buffer,
name: "Hackers",
members: ['cereal', 'burn', 'phreak', 'joey'],
avatar: { contentType: "image/jpg", length: avatarLen }
avatar: { contentType: "image/jpeg", length: avatarLen }
});
var groupInfoBuffer = groupInfo.encode().toArrayBuffer();
@ -93,7 +93,7 @@ describe("GroupBuffer", function() {
assert.strictEqual(group.name, "Hackers");
assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer);
assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']);
assert.strictEqual(group.avatar.contentType, "image/jpg");
assert.strictEqual(group.avatar.contentType, "image/jpeg");
assert.strictEqual(group.avatar.length, 255);
assert.strictEqual(group.avatar.data.byteLength, 255);
var avatarBytes = new Uint8Array(group.avatar.data);

View file

@ -22,7 +22,7 @@
* error: function(message, status, request) {...}
* });
*
* 1. https://github.com/WhisperSystems/WebSocket-Resources
* 1. https://github.com/signalapp/WebSocket-Resources
*
*/

107
main.js
View file

@ -37,11 +37,17 @@ function getMainWindow() {
// 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 startInTray = process.argv.some(arg => arg === '--start-in-tray');
const usingTrayIcon = startInTray || process.argv.some(arg => arg === '--use-tray-icon');
const config = require('./app/config');
const importMode = process.argv.some(arg => arg === '--import') || config.get('import');
const development = config.environment === 'development';
// Very important to put before the single instance check, since it is based on the
// userData directory.
const userConfig = require('./app/user_config');
@ -54,7 +60,7 @@ function showWindow() {
// 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/WhisperSystems/Signal-Desktop/issues/1429
// https://github.com/signalapp/Signal-Desktop/issues/1429
if (mainWindow.isVisible()) {
mainWindow.focus();
} else {
@ -83,7 +89,7 @@ if (!process.mas) {
if (shouldQuit) {
console.log('quitting; we are the second instance');
app.quit();
app.exit();
}
}
@ -119,6 +125,7 @@ function prepareURL(pathSegments) {
appInstance: process.env.NODE_APP_INSTANCE,
polyfillNotifications: polyfillNotifications ? true : undefined, // for stringify()
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy,
importMode: importMode ? true : undefined, // for stringify()
},
});
}
@ -157,10 +164,10 @@ function isVisible(window, bounds) {
const topClearOfUpperBound = window.y >= boundsY;
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
return rightSideClearOfLeftBound
&& leftSideClearOfRightBound
&& topClearOfUpperBound
&& topClearOfLowerBound;
return rightSideClearOfLeftBound &&
leftSideClearOfRightBound &&
topClearOfUpperBound &&
topClearOfLowerBound;
}
function createWindow() {
@ -216,6 +223,10 @@ function createWindow() {
mainWindow = new BrowserWindow(windowOptions);
function captureAndSaveWindowStats() {
if (!mainWindow) {
return;
}
const size = mainWindow.getSize();
const position = mainWindow.getPosition();
@ -277,8 +288,8 @@ function createWindow() {
// Emitted when the window is about to be closed.
mainWindow.on('close', (e) => {
// If the application is terminating, just do the default
if (windowState.shouldQuit()
|| config.environment === 'test' || config.environment === 'test-lib') {
if (windowState.shouldQuit() ||
config.environment === 'test' || config.environment === 'test-lib') {
return;
}
@ -315,11 +326,11 @@ function showDebugLog() {
}
function openReleaseNotes() {
shell.openExternal(`https://github.com/WhisperSystems/Signal-Desktop/releases/tag/v${app.getVersion()}`);
shell.openExternal(`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`);
}
function openNewBugForm() {
shell.openExternal('https://github.com/WhisperSystems/Signal-Desktop/issues/new');
shell.openExternal('https://github.com/signalapp/Signal-Desktop/issues/new');
}
function openSupportPage() {
@ -330,6 +341,24 @@ function openForums() {
shell.openExternal('https://whispersystems.discoursehosting.net/');
}
function setupWithImport() {
if (mainWindow) {
mainWindow.webContents.send('set-up-with-import');
}
}
function setupAsNewDevice() {
if (mainWindow) {
mainWindow.webContents.send('set-up-as-new-device');
}
}
function setupAsStandalone() {
if (mainWindow) {
mainWindow.webContents.send('set-up-as-standalone');
}
}
let aboutWindow;
function showAbout() {
@ -373,6 +402,8 @@ function showAbout() {
// Some APIs can only be used after this event occurs.
let ready = false;
app.on('ready', () => {
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
/* eslint-disable more/no-then */
let loggingSetupError;
logging.initialize().catch((error) => {
loggingSetupError = error;
@ -398,22 +429,31 @@ app.on('ready', () => {
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);
setupMenu();
});
/* eslint-enable more/no-then */
});
function setupMenu(options) {
const menuOptions = Object.assign({}, options, {
development,
showDebugLog,
showWindow,
showAbout,
openReleaseNotes,
openNewBugForm,
openSupportPage,
openForums,
setupWithImport,
setupAsNewDevice,
setupAsStandalone,
});
const template = createTemplate(menuOptions, locale.messages);
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
app.on('before-quit', () => {
windowState.markShouldQuit();
});
@ -422,9 +462,9 @@ app.on('before-quit', () => {
app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin'
|| config.environment === 'test'
|| config.environment === 'test-lib') {
if (process.platform !== 'darwin' ||
config.environment === 'test' ||
config.environment === 'test-lib') {
app.quit();
}
});
@ -447,6 +487,17 @@ ipc.on('set-badge-count', (event, count) => {
app.setBadgeCount(count);
});
ipc.on('remove-setup-menu-items', () => {
setupMenu();
});
ipc.on('add-setup-menu-items', () => {
setupMenu({
includeSetup: true,
});
});
ipc.on('draw-attention', () => {
if (process.platform === 'darwin') {
app.dock.bounce();

View file

@ -2,17 +2,17 @@
"name": "signal-desktop",
"productName": "Signal",
"description": "Private messaging from your desktop",
"repository": "https://github.com/WhisperSystems/Signal-Desktop.git",
"version": "1.3.0",
"repository": "https://github.com/signalapp/Signal-Desktop.git",
"version": "1.5.0-beta.1",
"license": "GPL-3.0",
"author": {
"name": "Open Whisper Systems",
"email": "support@whispersystems.org"
"email": "support@signal.org"
},
"main": "main.js",
"scripts": {
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
"test": "npm run eslint && npm run test-server && grunt test",
"test": "npm run eslint && npm run test-server && grunt test && npm run test-modules",
"lint": "grunt jshint",
"start": "electron .",
"asarl": "asar l release/mac/Signal.app/Contents/Resources/app.asar",
@ -21,13 +21,15 @@
"build": "build --em.environment=$SIGNAL_ENV",
"dist": "npm run generate && npm run build",
"pack": "npm run generate && npm run build -- --dir",
"prepare-build": "node prepare_build.js",
"prepare-beta-build": "node prepare_beta_build.js",
"prepare-import-build": "node prepare_import_build.js",
"pack-prod": "SIGNAL_ENV=production npm run pack",
"dist-prod": "SIGNAL_ENV=production npm run dist",
"dist-prod-all": "SIGNAL_ENV=production npm run dist -- -mwl",
"build-release": "SIGNAL_ENV=production npm run build -- --config.directories.output=release",
"build-mas-release": "npm run build-release -- -m --config.mac.target=mas",
"build-mas-dev": "npm run build-release -- -m --config.mac.target=mas --config.type=development",
"grunt": "grunt",
"prep-mac-release": "npm run build-release -- -m --dir",
"prep-release": "npm run generate && grunt prep-release && npm run build-release && npm run build-mas-release && grunt test-release",
"release-mac": "npm run build-release -- -m --prepackaged release/mac/Signal*.app --publish=always",
@ -36,10 +38,14 @@
"release": "npm run release-mac && npm run release-win && npm run release-lin",
"test-server": "mocha --recursive test/server",
"test-server-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/server",
"test-modules": "mocha --recursive test/modules",
"eslint": "eslint .",
"open-coverage": "open coverage/lcov-report/index.html"
},
"dependencies": {
"blob-util": "^1.3.0",
"blueimp-canvas-to-blob": "^3.14.0",
"blueimp-load-image": "^2.18.0",
"bunyan": "^1.8.12",
"config": "^1.28.1",
"electron-config": "^1.0.0",
@ -62,19 +68,21 @@
"rimraf": "^2.6.2",
"semver": "^5.4.1",
"spellchecker": "^3.4.4",
"testcheck": "^1.0.0-rc.2",
"websocket": "^1.0.25"
},
"devDependencies": {
"asar": "^0.14.0",
"bower": "^1.8.2",
"chai": "^4.1.2",
"electron": "1.7.11",
"electron": "1.7.12",
"electron-builder": "^19.53.7",
"electron-icon-maker": "0.0.3",
"electron-publisher-s3": "^19.53.7",
"eslint": "^4.14.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-more": "^0.3.1",
"extract-zip": "^1.6.6",
"grunt": "^1.0.1",
"grunt-cli": "^1.2.0",
@ -87,6 +95,7 @@
"grunt-jscs": "^3.0.1",
"grunt-sass": "^2.0.0",
"mocha": "^4.1.0",
"mocha-testcheck": "^1.0.0-rc.0",
"node-sass-import-once": "^1.2.0",
"nyc": "^11.4.1",
"spectron": "^3.7.2",

View file

@ -42,6 +42,26 @@
Whisper.events.trigger('showDebugLog');
});
ipc.on('set-up-with-import', function() {
Whisper.events.trigger('setupWithImport');
});
ipc.on('set-up-as-new-device', function() {
Whisper.events.trigger('setupAsNewDevice');
});
ipc.on('set-up-as-standalone', function() {
Whisper.events.trigger('setupAsStandalone');
});
window.addSetupMenuItems = function() {
ipc.send('add-setup-menu-items');
}
window.removeSetupMenuItems = function() {
ipc.send('remove-setup-menu-items');
}
// We pull these dependencies in now, from here, because they have Node.js dependencies
require('./js/logging');
@ -60,6 +80,8 @@
window.nodeSetImmediate(function() {});
}, 1000);
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
window.loadImage = require('blueimp-load-image');
window.ProxyAgent = require('proxy-agent');
window.EmojiConvertor = require('emoji-js');
window.emojiData = require('emoji-datasource');
@ -70,6 +92,16 @@
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
window.nodeNotifier = require('node-notifier');
const { autoOrientImage } = require('./js/modules/auto_orient_image');
window.autoOrientImage = autoOrientImage;
// ES2015+ modules
window.Signal = window.Signal || {};
window.Signal.Types = window.Signal.Types || {};
window.Signal.Types.Attachment = require('./js/modules/types/attachment');
window.Signal.Types.Message = require('./js/modules/types/message');
window.Signal.Types.MIME = require('./js/modules/types/mime');
// We pull this in last, because the native module involved appears to be sensitive to
// /tmp mounted as noexec on Linux.
require('./js/spell_check');

View file

@ -17,7 +17,7 @@ if (!beta.test(version)) {
process.exit();
}
console.log('prepare_build: updating package.json for beta build');
console.log('prepare_beta_build: updating package.json');
// -------

60
prepare_import_build.js Normal file
View file

@ -0,0 +1,60 @@
const fs = require('fs');
const _ = require('lodash');
const packageJson = require('./package.json');
const defaultConfig = require('./config/default.json');
function checkValue(object, objectPath, expected) {
const actual = _.get(object, objectPath);
if (actual !== expected) {
throw new Error(`${objectPath} was ${actual}; expected ${expected}`);
}
}
// You might be wondering why this file is necessary. We have some very specific
// requirements around our import-flavor builds. They need to look exactly the same as
// normal builds, but they must immediately open into import mode. So they need a
// slight config tweak, and then a change to the .app/.exe name (note: we do NOT want to
// change where data is stored or anything, since that would make these builds
// incompatible with the mainline builds) So we just change the artifact name.
//
// Another key thing to know about these builds is that we should not upload the
// latest.yml (windows) and latest-mac.yml (mac) that go along with the executables.
// This would interrupt the normal install flow for users installing from
// signal.org/download. So any release script will need to upload these files manually
// instead of relying on electron-builder, which will upload everything.
// -------
console.log('prepare_import_build: updating config/default.json');
const IMPORT_PATH = 'import';
const IMPORT_START_VALUE = false;
const IMPORT_END_VALUE = true;
checkValue(defaultConfig, IMPORT_PATH, IMPORT_START_VALUE);
_.set(defaultConfig, IMPORT_PATH, IMPORT_END_VALUE);
// -------
console.log('prepare_import_build: updating package.json');
const MAC_ASSET_PATH = 'build.mac.artifactName';
const MAC_ASSET_START_VALUE = '${name}-mac-${version}.${ext}';
const MAC_ASSET_END_VALUE = '${name}-mac-${version}-import.${ext}';
const WIN_ASSET_PATH = 'build.win.artifactName';
const WIN_ASSET_START_VALUE = '${name}-win-${version}.${ext}';
const WIN_ASSET_END_VALUE = '${name}-win-${version}-import.${ext}';
checkValue(packageJson, MAC_ASSET_PATH, MAC_ASSET_START_VALUE);
checkValue(packageJson, WIN_ASSET_PATH, WIN_ASSET_START_VALUE);
_.set(packageJson, MAC_ASSET_PATH, MAC_ASSET_END_VALUE);
_.set(packageJson, WIN_ASSET_PATH, WIN_ASSET_END_VALUE);
// ---
fs.writeFileSync('./config/default.json', JSON.stringify(defaultConfig, null, ' '));
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' '));

View file

@ -1,4 +1,4 @@
package textsecure;
package signalservice;
message ProvisioningUuid {
optional string uuid = 1;

View file

@ -1,7 +1,8 @@
package textsecure;
// Source: https://github.com/signalapp/libsignal-service-java/blob/4684a49b2ed8f32be619e0d0eea423626b6cb2cb/protobuf/SignalService.proto
package signalservice;
option java_package = "org.whispersystems.textsecure.internal.push";
option java_outer_classname = "TextSecureProtos";
option java_package = "org.whispersystems.signalservice.internal.push";
option java_outer_classname = "SignalServiceProtos";
message Envelope {
enum Type {
@ -22,40 +23,13 @@ message Envelope {
}
message Content {
optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2;
optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4;
optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2;
optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4;
optional ReceiptMessage receiptMessage = 5;
}
message ReceiptMessage {
enum Type {
DELIVERY = 0;
READ = 1;
}
optional Type type = 1;
repeated uint64 timestamps = 2;
}
message NullMessage {
optional bytes padding = 1;
}
message Verified {
enum State {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
optional string destination = 1;
optional bytes identityKey = 2;
optional State state = 3;
optional bytes nullMessage = 4;
}
message CallMessage {
message Offer {
optional uint64 id = 1;
@ -92,7 +66,7 @@ message CallMessage {
message DataMessage {
enum Flags {
END_SESSION = 1;
END_SESSION = 1;
EXPIRATION_TIMER_UPDATE = 2;
PROFILE_KEY_UPDATE = 4;
}
@ -103,6 +77,34 @@ message DataMessage {
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
}
message NullMessage {
optional bytes padding = 1;
}
message ReceiptMessage {
enum Type {
DELIVERY = 0;
READ = 1;
}
optional Type type = 1;
repeated uint64 timestamp = 2;
}
message Verified {
enum State {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
optional string destination = 1;
optional bytes identityKey = 2;
optional State state = 3;
optional bytes nullMessage = 4;
}
message SyncMessage {
@ -115,7 +117,7 @@ message SyncMessage {
message Contacts {
optional AttachmentPointer blob = 1;
optional bool isComplete = 2 [default = false];
optional bool complete = 2 [default = false];
}
message Groups {
@ -143,19 +145,19 @@ message SyncMessage {
optional uint64 timestamp = 2;
}
message Settings {
message Configuration {
optional bool readReceipts = 1;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
optional Request request = 4;
repeated Read read = 5;
optional Blocked blocked = 6;
optional Verified verified = 7;
optional bytes padding = 8;
optional Settings settings = 9;
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
optional Request request = 4;
repeated Read read = 5;
optional Blocked blocked = 6;
optional Verified verified = 7;
optional Configuration configuration = 9;
optional bytes padding = 8;
}
message AttachmentPointer {
@ -171,6 +173,8 @@ message AttachmentPointer {
optional bytes digest = 6;
optional string fileName = 7;
optional uint32 flags = 8;
optional uint32 width = 9;
optional uint32 height = 10;
}
message GroupContext {
@ -194,12 +198,14 @@ message ContactDetails {
optional uint32 length = 2;
}
optional string number = 1;
optional string name = 2;
optional Avatar avatar = 3;
optional string color = 4;
optional Verified verified = 5;
optional bytes profileKey = 6;
optional string number = 1;
optional string name = 2;
optional Avatar avatar = 3;
optional string color = 4;
optional Verified verified = 5;
optional bytes profileKey = 6;
optional bool blocked = 7;
optional uint32 expireTimer = 8;
}
message GroupDetails {
@ -208,9 +214,10 @@ message GroupDetails {
optional uint32 length = 2;
}
optional bytes id = 1;
optional string name = 2;
repeated string members = 3;
optional Avatar avatar = 4;
optional bool active = 5 [default = true];
optional bytes id = 1;
optional string name = 2;
repeated string members = 3;
optional Avatar avatar = 4;
optional bool active = 5 [default = true];
optional uint32 expireTimer = 6;
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package textsecure;
package signalservice;
option java_package = "org.whispersystems.websocket.messages.protobuf";
@ -42,4 +42,4 @@ message WebSocketMessage {
optional Type type = 1;
optional WebSocketRequestMessage request = 2;
optional WebSocketResponseMessage response = 3;
}
}

View file

@ -1,4 +1,4 @@
package textsecure;
package signalservice;
option java_package = "org.whispersystems.libsignal.protocol";
option java_outer_classname = "WhisperProtos";

View file

@ -218,7 +218,7 @@ button.hamburger {
}
.dropoff {
outline: solid 1px #2090ea;
outline: solid 1px $blue;
}
$avatar-size: 44px;
@ -609,6 +609,281 @@ input[type=text], input[type=search], textarea {
}
}
.full-screen-flow {
z-index: 1000;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
font-family: roboto-light;
color: black;
a {
color: $blue;
}
background: linear-gradient(
to bottom,
rgb(238,238,238) 0%, // (1 - 0.41) * 255 + 0.41 * 213
rgb(243,243,243) 12%, // (1 - 0.19) * 255 + 0.19 * 191
rgb(255,255,255) 27%,
rgb(255,255,255) 60%,
rgb(249,249,249) 85%, // (1 - 0.19) * 255 + 0.19 * 222
rgb(213,213,213) 100% // (1 - 0.27) * 255 + 0.27 * 98
);
display: flex;
align-items: center;
text-align: center;
font-size: 10pt;
input {
margin-top: 1em;
font-size: 12pt;
font-family: roboto-light;
border: 2px solid $blue;
padding: 0.5em;
text-align: center;
width: 20em;
}
@media (min-height: 750px) and (min-width: 700px) {
font-size: 14pt;
input {
font-size: 16pt;
}
}
#qr {
display: inline-block;
&.ready {
border: 5px solid $blue;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
img {
height: 20em;
border: 5px solid white;
}
@media (max-height: 475px) {
img {
width: 8em;
height: 8em;
}
}
.dot {
width: 14px;
height: 14px;
border: 3px solid $blue;
border-radius: 50%;
float: left;
margin: 0 6px;
transform: scale(0);
animation: loading 1500ms ease infinite 0ms;
&:nth-child(2) {
animation: loading 1500ms ease infinite 333ms;
}
&:nth-child(3) {
animation: loading 1500ms ease infinite 666ms;
}
}
canvas {
display: none;
}
}
.os-icon {
height: 3em;
width: 3em;
vertical-align: text-bottom;
display: inline-block;
margin: 0.5em;
&.apple {
@include color-svg('../images/apple.svg', black);
}
&.android {
@include color-svg('../images/android.svg', black);
}
}
.header {
font-weight: normal;
margin-bottom: 1.5em;
font-size: 20pt;
@media (min-height: 750px) and (min-width: 700px) {
font-size: 28pt;
}
}
.body-text {
max-width: 22em;
text-align: left;
margin-left: auto;
margin-right: auto;
}
.body-text-wide {
max-width: 30em;
text-align: left;
margin-left: auto;
margin-right: auto;
}
.step {
height: 100%;
width: 100%;
padding: 70px 0 50px;
}
.step-body {
margin-left: auto;
margin-right: auto;
max-width: 35em;
}
.inner {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
}
.banner-image {
margin: 1em;
display: none;
@media (min-height: 550px) {
display: inline-block;
height: 10em;
width: 10em;
}
}
.banner-icon {
display: none;
margin: 1em;
// 640px by 338px is the smallest the window can go
@media (min-height: 550px) {
display: inline-block;
height: 10em;
width: 10em;
}
// generic
&.check-circle-outline {
@include color-svg('../images/check-circle-outline.svg', #DEDEDE);
}
&.alert-outline {
@include color-svg('../images/alert-outline.svg', #DEDEDE);
}
// import and export
&.folder-outline {
@include color-svg('../images/folder-outline.svg', #DEDEDE);
}
&.import {
@include color-svg('../images/import.svg', #DEDEDE);
}
&.export {
@include color-svg('../images/export.svg', #DEDEDE);
}
// registration process
&.lead-pencil {
@include color-svg('../images/lead-pencil.svg', #DEDEDE);
}
&.sync {
@include color-svg('../images/sync.svg', #DEDEDE);
}
}
.button {
cursor: pointer;
display: inline-block;
border: none;
min-width: 300px;
padding: 0.75em;
margin-top: 1em;
color: white;
background: $blue;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
font-size: 12pt;
@media (min-height: 750px) and (min-width: 700px) {
font-size: 20pt;
}
}
a.link {
display: block;
cursor: pointer;
text-decoration: underline;
margin: 0.5em;
color: #2090ea;
}
.progress {
text-align: center;
padding: 1em;
width: 80%;
margin: auto;
.bar-container {
height: 1em;
margin: 1em;
background-color: $grey_l;
}
.bar {
width: 100%;
height: 100%;
background-color: $blue_l;
transition: width 0.25s;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
}
.nav {
width: 100%;
bottom: 50px;
margin-top: auto;
padding-bottom: 2em;
padding-left: 20px;
padding-right: 20px;
.instructions {
text-align: left;
margin-left: auto;
margin-right: auto;
margin-bottom: 2em;
margin-top: 2em;
max-width: 30em;
}
.instructions:after {
clear: both;
}
.android {
float: left;
}
.apple {
float: right;
}
.label {
float: left;
}
.body {
float: left;
}
}
}
//yellow border fix
.inbox:focus {
outline: none;

View file

@ -11,6 +11,10 @@
hr {
margin: 10px 0;
}
.device-name-settings {
text-align: center;
margin-bottom: 1em;
}
.syncSettings {
button {
float: right;

View file

@ -6,335 +6,6 @@
background: url("../images/flags.png");
}
.install {
height: 100%;
background: #2090ea;
color: white;
text-align: center;
font-size: 16px;
overflow: auto;
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.main {
padding: 70px 0 50px;
}
.hidden {
display: none;
}
.step {
height: 100%;
}
.inner {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
.step-body {
margin-top: auto;
width: 100%;
max-width: 600px;
}
}
#signal-computer,
#signal-phone {
max-width: 50%;
max-height: 250px;
}
p {
max-width: 35em;
margin: 1em auto;
padding: 0 1em;
line-height: 1.5em;
font-size: 1.2em;
font-weight: bold;
}
a {
cursor: pointer;
&, &:visited, &:hover {
text-decoration: none;
}
}
.button {
display: inline-block;
text-transform: uppercase;
border: none;
font-weight: bold;
min-width: 300px;
padding: 0.5em;
margin: 0.5em 0;
background: white;
color: $blue;
}
.nav {
width: 100%;
bottom: 50px;
margin-top: auto;
padding: 20px;
.dot-container {
margin-top: 3em;
}
.dot {
display: inline-block;
cursor: pointer;
margin: 10px;
width: 20px;
height: 20px;
border-radius: 10px;
background: white;
border: solid 5px $blue;
&.selected {
background: $blue_l;
}
}
}
&.install-choice .nav {
top: 20px;
margin-bottom: auto;
}
.link {
&:hover, &:focus {
background: rgba(255,255,255,0.3);
outline: none;
}
&, &:visited, &:hover {
padding: 0 3px;
color: white;
font-weight: bold;
border-bottom: dashed 2px white;
text-decoration: none;
}
}
.container {
min-width: 650px;
}
h1 {
font-size: 30pt;
font-weight: normal;
padding-bottom: 10px;
}
h3.step {
margin-top: 0;
font-weight: bold;
}
.help {
border-top: 2px solid $grey_l;
padding: 1.5em 0.1em;
}
.install {
display: inline-block;
margin-top: 90px;
}
#qr {
display: inline-block;
min-height: 266px;
img {
border: 5px solid white;
}
canvas {
display: none;
}
}
#device-name {
border: none;
border-bottom: 1px solid white;
padding: 8px;
background: transparent;
color: white;
font-weight: bold;
text-align: center;
&::selection, a::selection {
color: $grey_d;
background: white;
}
&::-moz-selection, a::-moz-selection {
color: $grey_d;
background: white;
}
&:focus {
outline: none;
}
&:hover, &:focus {
background: rgba(255,255,255,0.1);
}
}
#verifyCode,
#code,
#number {
box-sizing: border-box;
width: 100%;
display: block;
margin-bottom: 0.5em;
text-align: center;
}
#request-voice,
#request-sms {
box-sizing: border-box;
}
#request-sms {
width: 57%;
float: right;
}
#request-voice {
width: 40%;
float: left;
}
.number-container {
position: relative;
margin-bottom: 0.5em;
}
.number-container .intl-tel-input,
.number-container .number {
width: 100%;
}
.number-container::after {
visibility: hidden;
content: ' ';
display: inline-block;
border-radius: 1.5em;
width: 1.5em;
height: 1.5em;
line-height: 1.5em;
color: #ffffff;
position: absolute;
top: 0;
left: 100%;
margin: 3px 8px;
text-align: center;
}
.number-container.valid::after {
visibility: visible;
content: '';
background-color: #0f9d58;
color: #ffffff;
}
.number-container.invalid::after {
visibility: visible;
content: '!';
background-color: #f44336;
color: #ffffff;
}
#error {
color: white;
font-weight: bold;
padding: 0.5em;
text-align: center;
}
#error { background-color: #f44336; }
#error:before {
content: '\26a0';
padding-right: 0.5em;
}
.narrow {
margin: auto;
box-sizing: border-box;
width: 275px;
max-width: 100%;
}
ul.country-list {
min-width: 197px !important;
}
.confirmation-dialog, .progress-dialog {
padding: 1em;
text-align: left;
}
.number { text-align: center; }
.confirmation-dialog {
button {
float: right;
margin-left: 10px;
}
}
.progress-dialog {
text-align: center;
padding: 1em;
width: 100%;
max-width: 600px;
margin: auto;
.status { padding: 1em; }
.bar-container {
height: 1em;
background-color: $grey_l;
border: solid 1px white;
}
.bar {
width: 0;
height: 100%;
background-color: $blue_l;
transition: width 0.25s;
&.active {
}
}
}
.modal-container {
display: none;
position: absolute;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.1);
top: 0;
padding-top: 10em;
text-align: center;
.modal-main {
display: inline-block;
width: 80%;
max-width: 500px;
border: solid 2px $blue;
background: white;
margin: 10% auto;
box-shadow: 0 0 5px 3px rgba(darken($blue, 30%), 0.2);
h4 {
background-color: $blue;
color: white;
padding: 1em;
margin: 0;
text-align: left;
}
}
}
}
.intl-tel-input .country-list {
text-align: left;
}

View file

@ -450,7 +450,7 @@
</div>
</script>
<script type='text/x-tmpl-mustache' id='link_to_support'>
<a href='http://support.whispersystems.org/hc/articles/213134107' target='_blank'>
<a href='http://support.signal.org/hc/articles/213134107' target='_blank'>
{{ learnMore }}
</a>
</script>
@ -476,7 +476,7 @@
</div>
<p>
<a class='report-link' target='_blank'
href='https://github.com/WhisperSystems/Signal-Desktop/issues/new/'>
href='https://github.com/signalapp/Signal-Desktop/issues/new/'>
{{ reportIssue }}
</a>
</p>

6
test/modules/.eslintrc Normal file
View file

@ -0,0 +1,6 @@
{
"globals": {
"check": true,
"gen": true
}
}

View file

@ -0,0 +1,246 @@
require('mocha-testcheck').install();
const { assert } = require('chai');
const Attachment = require('../../../js/modules/types/attachment');
describe('Attachment', () => {
describe('upgradeSchema', () => {
it('should upgrade an unversioned attachment to the latest version', async () => {
const input = {
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
};
const expected = {
contentType: 'application/json',
data: null,
fileName: 'test\uFFFDfig.exe',
size: 1111,
schemaVersion: Attachment.CURRENT_SCHEMA_VERSION,
};
const actual = await Attachment.upgradeSchema(input);
assert.deepEqual(actual, expected);
});
context('with multiple upgrade steps', () => {
it('should return last valid attachment when any upgrade step fails', async () => {
const input = {
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
};
const expected = {
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
schemaVersion: 1,
hasUpgradedToVersion1: true,
};
const v1 = async attachment =>
Object.assign({}, attachment, { hasUpgradedToVersion1: true });
const v2 = async () => {
throw new Error('boom');
};
const v3 = async attachment =>
Object.assign({}, attachment, { hasUpgradedToVersion3: true });
const toVersion1 = Attachment.withSchemaVersion(1, v1);
const toVersion2 = Attachment.withSchemaVersion(2, v2);
const toVersion3 = Attachment.withSchemaVersion(3, v3);
const upgradeSchema = async attachment =>
toVersion3(await toVersion2(await toVersion1(attachment)));
const actual = await upgradeSchema(input);
assert.deepEqual(actual, expected);
});
it('should skip out-of-order upgrade steps', async () => {
const input = {
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
};
const expected = {
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
schemaVersion: 2,
hasUpgradedToVersion1: true,
hasUpgradedToVersion2: true,
};
const v1 = async attachment =>
Object.assign({}, attachment, { hasUpgradedToVersion1: true });
const v2 = async attachment =>
Object.assign({}, attachment, { hasUpgradedToVersion2: true });
const v3 = async attachment =>
Object.assign({}, attachment, { hasUpgradedToVersion3: true });
const toVersion1 = Attachment.withSchemaVersion(1, v1);
const toVersion2 = Attachment.withSchemaVersion(2, v2);
const toVersion3 = Attachment.withSchemaVersion(3, v3);
// NOTE: We upgrade to 3 before 2, i.e. the pipeline should abort:
const upgradeSchema = async attachment =>
toVersion2(await toVersion3(await toVersion1(attachment)));
const actual = await upgradeSchema(input);
assert.deepEqual(actual, expected);
});
});
});
describe('withSchemaVersion', () => {
it('should require a version number', () => {
const toVersionX = () => {};
assert.throws(
() => Attachment.withSchemaVersion(toVersionX, 2),
'`schemaVersion` must be a number'
);
});
it('should require an upgrade function', () => {
assert.throws(
() => Attachment.withSchemaVersion(2, 3),
'`upgrade` must be a function'
);
});
it('should skip upgrading if attachment has already been upgraded', async () => {
const upgrade = async attachment =>
Object.assign({}, attachment, { foo: true });
const upgradeWithVersion = Attachment.withSchemaVersion(3, upgrade);
const input = {
contentType: 'image/gif',
data: null,
fileName: 'foo.gif',
size: 1111,
schemaVersion: 4,
};
const actual = await upgradeWithVersion(input);
assert.deepEqual(actual, input);
});
it('should return original attachment if upgrade function throws', async () => {
const upgrade = async () => {
throw new Error('boom!');
};
const upgradeWithVersion = Attachment.withSchemaVersion(3, upgrade);
const input = {
contentType: 'image/gif',
data: null,
fileName: 'foo.gif',
size: 1111,
};
const actual = await upgradeWithVersion(input);
assert.deepEqual(actual, input);
});
it('should return original attachment if upgrade function returns null', async () => {
const upgrade = async () => null;
const upgradeWithVersion = Attachment.withSchemaVersion(3, upgrade);
const input = {
contentType: 'image/gif',
data: null,
fileName: 'foo.gif',
size: 1111,
};
const actual = await upgradeWithVersion(input);
assert.deepEqual(actual, input);
});
});
describe('replaceUnicodeOrderOverrides', () => {
it('should sanitize left-to-right order override character', async () => {
const input = {
contentType: 'image/jpeg',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
schemaVersion: 1,
};
const expected = {
contentType: 'image/jpeg',
data: null,
fileName: 'test\uFFFDfig.exe',
size: 1111,
schemaVersion: 1,
};
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
assert.deepEqual(actual, expected);
});
it('should sanitize right-to-left order override character', async () => {
const input = {
contentType: 'image/jpeg',
data: null,
fileName: 'test\u202Efig.exe',
size: 1111,
schemaVersion: 1,
};
const expected = {
contentType: 'image/jpeg',
data: null,
fileName: 'test\uFFFDfig.exe',
size: 1111,
schemaVersion: 1,
};
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
assert.deepEqual(actual, expected);
});
it('should sanitize multiple override characters', async () => {
const input = {
contentType: 'image/jpeg',
data: null,
fileName: 'test\u202e\u202dlol\u202efig.exe',
size: 1111,
schemaVersion: 1,
};
const expected = {
contentType: 'image/jpeg',
data: null,
fileName: 'test\uFFFD\uFFFDlol\uFFFDfig.exe',
size: 1111,
schemaVersion: 1,
};
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
assert.deepEqual(actual, expected);
});
const hasNoUnicodeOrderOverrides = value =>
!value.includes('\u202D') && !value.includes('\u202E');
check.it(
'should ignore non-order-override characters',
gen.string.suchThat(hasNoUnicodeOrderOverrides),
(fileName) => {
const input = {
contentType: 'image/jpeg',
data: null,
fileName,
size: 1111,
schemaVersion: 1,
};
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
assert.deepEqual(actual, input);
}
);
});
});

View 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));
});
});
});
});

View file

@ -1,3 +1,6 @@
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
/* eslint-disable more/no-then */
const fs = require('fs');
const path = require('path');

View file

@ -2,18 +2,33 @@
* vim: ts=4:sw=4:expandtab
*/
describe('LastSeenIndicatorView', function() {
// TODO: in electron branch, where we have access to real i18n, test rendered HTML
it('renders provided count', function() {
var view = new Whisper.LastSeenIndicatorView({count: 10});
assert.equal(view.count, 10);
view.render();
assert.match(view.$el.html(), /10 Unread Messages/);
});
it('renders count of 1', function() {
var view = new Whisper.LastSeenIndicatorView({count: 1});
assert.equal(view.count, 1);
view.render();
assert.match(view.$el.html(), /1 Unread Message/);
});
it('increments count', function() {
var view = new Whisper.LastSeenIndicatorView({count: 4});
assert.equal(view.count, 4);
view.render();
assert.match(view.$el.html(), /4 Unread Messages/);
view.increment(3);
assert.equal(view.count, 7);
view.render();
assert.match(view.$el.html(), /7 Unread Messages/);
});
});

View file

@ -2,13 +2,11 @@
* vim: ts=4:sw=4:expandtab
*/
describe('ScrollDownButtonView', function() {
// TODO: in electron branch, where we have access to real i18n, uncomment assertions against real strings
it('renders with count = 0', function() {
var view = new Whisper.ScrollDownButtonView();
view.render();
assert.equal(view.count, 0);
// assert.match(view.$el.html(), /Scroll to bottom/);
assert.match(view.$el.html(), /Scroll to bottom/);
});
it('renders with count = 1', function() {
@ -16,7 +14,7 @@ describe('ScrollDownButtonView', function() {
view.render();
assert.equal(view.count, 1);
assert.match(view.$el.html(), /new-messages/);
// assert.match(view.$el.html(), /New message below/);
assert.match(view.$el.html(), /New message below/);
});
it('renders with count = 2', function() {
@ -25,7 +23,7 @@ describe('ScrollDownButtonView', function() {
assert.equal(view.count, 2);
assert.match(view.$el.html(), /new-messages/);
// assert.match(view.$el.html(), /New messages below/);
assert.match(view.$el.html(), /New messages below/);
});
it('increments count and re-renders', function() {

View file

@ -437,6 +437,17 @@ bl@^1.0.0:
dependencies:
readable-stream "^2.0.5"
blob-util@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-1.3.0.tgz#dbb4e8caffd50b5720d347e1169b6369ba34fe95"
dependencies:
blob "0.0.4"
native-or-lie "1.0.2"
blob@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
block-stream@*:
version "0.0.9"
resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
@ -457,6 +468,14 @@ bluebird@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
blueimp-canvas-to-blob@^3.14.0:
version "3.14.0"
resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.14.0.tgz#ea075ffbfb1436607b0c75e951fb1ceb3ca0288e"
blueimp-load-image@^2.18.0:
version "2.18.0"
resolved "https://registry.yarnpkg.com/blueimp-load-image/-/blueimp-load-image-2.18.0.tgz#03b93687eb382a7136cfbcbd4f0e936b6763fc0e"
bmp-js@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.1.tgz#5ad0147099d13a9f38aa7b99af1d6e78666ed37f"
@ -1516,9 +1535,9 @@ electron-updater@^2.19.0:
semver "^5.4.1"
source-map-support "^0.5.0"
electron@1.7.11:
version "1.7.11"
resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.11.tgz#993b6aa79e0e79a7cfcc369f4c813fbd9a0b08d9"
electron@1.7.12:
version "1.7.12"
resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.12.tgz#dcc61a2c1b0c3df25f68b3425379a01abd01190e"
dependencies:
"@types/node" "^7.0.18"
electron-download "^3.0.1"
@ -1642,6 +1661,10 @@ eslint-plugin-import@^2.8.0:
minimatch "^3.0.3"
read-pkg-up "^2.0.0"
eslint-plugin-more@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-more/-/eslint-plugin-more-0.3.1.tgz#ff688fb3fa8f153c8bfd5d70c15a68dc222a1b31"
eslint-restricted-globals@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
@ -2606,6 +2629,10 @@ ignore@^3.3.3:
version "3.3.7"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
import-lazy@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
@ -3150,6 +3177,12 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lie@*:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.2.0.tgz#4f13f2f8bbb027d383db338c43043545791aa8dc"
dependencies:
immediate "~3.0.5"
livereload-js@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2"
@ -3439,6 +3472,12 @@ mksnapshot@^0.3.0:
fs-extra "0.26.7"
request "^2.79.0"
mocha-testcheck@^1.0.0-rc.0:
version "1.0.0-rc.0"
resolved "https://registry.yarnpkg.com/mocha-testcheck/-/mocha-testcheck-1.0.0-rc.0.tgz#05e50203043be1537aef2a87dd96ccd447702773"
dependencies:
testcheck "^1.0.0-rc"
mocha@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794"
@ -3490,6 +3529,12 @@ nan@^2.0.0, nan@^2.3.2, nan@^2.3.3:
version "2.6.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
native-or-lie@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/native-or-lie/-/native-or-lie-1.0.2.tgz#c870ee0ba0bf0ff11350595d216cfea68a6d8086"
dependencies:
lie "*"
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -4947,6 +4992,10 @@ test-exclude@^4.1.1:
read-pkg-up "^1.0.1"
require-main-filename "^1.0.1"
testcheck@^1.0.0-rc, testcheck@^1.0.0-rc.2:
version "1.0.0-rc.2"
resolved "https://registry.yarnpkg.com/testcheck/-/testcheck-1.0.0-rc.2.tgz#11356a25b84575efe0b0857451e85b5fa74ee4e4"
text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"