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

View file

@ -11,6 +11,10 @@ module.exports = {
'airbnb-base', 'airbnb-base',
], ],
plugins: [
'more',
],
rules: { rules: {
'comma-dangle': ['error', { 'comma-dangle': ['error', {
arrays: 'always-multiline', arrays: 'always-multiline',
@ -21,7 +25,7 @@ module.exports = {
}], }],
// putting params on their own line helps stay within line length limit // putting params on their own line helps stay within line length limit
'function-paren-newline': ['error', 'consistent'], 'function-paren-newline': ['error', 'multiline'],
// 90 characters allows three+ side-by-side screens on a standard-size monitor // 90 characters allows three+ side-by-side screens on a standard-size monitor
'max-len': ['error', { 'max-len': ['error', {
@ -29,6 +33,9 @@ module.exports = {
ignoreUrls: true, ignoreUrls: true,
}], }],
// encourage consistent use of `async` / `await` instead of `then`
'more/no-then': 'error',
// it helps readability to put public API at top, // it helps readability to put public API at top,
'no-use-before-define': 'off', 'no-use-before-define': 'off',
@ -37,5 +44,8 @@ module.exports = {
// though we have a logger, we still remap console to log to disk // though we have a logger, we still remap console to log to disk
'no-console': 'off', 'no-console': 'off',
// 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/ --> <!-- Instructions for finding your OS version are here: http://whatsmyos.com/ -->
Linked device version: Linked device version:
<!-- Android: Settings -> Advanced, iOS: Settings -> About --> <!-- Android: Settings -> Advanced, iOS: Settings -> General -> About -->
### Link to debug log ### Link to debug log

View file

@ -8,7 +8,7 @@ install:
- yarn install - yarn install
script: script:
- yarn run generate - yarn run generate
- yarn prepare-build - yarn prepare-beta-build
- yarn eslint - yarn eslint
- yarn test-server - yarn test-server
- yarn lint - 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. 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 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 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! 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 ## Developer Setup
First, you'll need [Node.js](https://nodejs.org/) which matches our current version. 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 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 desired Node.js version. [nvm for windows](https://github.com/coreybutler/nvm-windows) is
still useful, but it doesn't support `.nvmrc` files. 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: 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 cd Signal-Desktop
npm install -g yarn # (only if you don't already have yarn) 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) 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`: 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) **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 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) the [Android](https://github.com/signalapp/Signal-Android/blob/master/BUILDING.md)
and [iOS](https://github.com/WhisperSystems/Signal-iOS/blob/master/BUILDING.md) projects. 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 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 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/Mp3LameEncoder.min.js',
'!js/libsignal-protocol-worker.js', '!js/libsignal-protocol-worker.js',
'!js/components.js', '!js/components.js',
'!js/modules/**/*.js',
'!js/signal_protocol_store.js', '!js/signal_protocol_store.js',
'_locales/**/*' '_locales/**/*'
], ],
@ -174,8 +175,10 @@ module.exports = function(grunt) {
'!js/Mp3LameEncoder.min.js', '!js/Mp3LameEncoder.min.js',
'!js/libsignal-protocol-worker.js', '!js/libsignal-protocol-worker.js',
'!js/components.js', '!js/components.js',
'!js/modules/**/*.js',
'test/**/*.js', 'test/**/*.js',
'!test/blanket_mocha.js', '!test/blanket_mocha.js',
'!test/modules/**/*.js',
'!test/test.js', '!test/test.js',
] ]
} }
@ -282,7 +285,7 @@ module.exports = function(grunt) {
var https = require('https'); var https = require('https');
var urlBase = "https://s3-us-west-1.amazonaws.com/signal-desktop-builds"; 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 sha = gitinfo.local.branch.current.SHA;
var files = [{ var files = [{
zip: packageJson.name + '-' + packageJson.version + '.zip', 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
========================== ==========================
Signal Desktop is an Electron application that links with your Signal Desktop is an Electron application that links with your
[Signal Android](https://github.com/WhisperSystems/Signal-Android) [Signal Android](https://github.com/signalapp/Signal-Android)
or [Signal iOS](https://github.com/WhisperSystems/Signal-iOS) app. or [Signal iOS](https://github.com/signalapp/Signal-iOS) app.
## Install production version: https://signal.org/download/ ## 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! 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 ## Contributing Translations
@ -38,7 +38,7 @@ https://www.transifex.com/projects/p/signal-desktop
## Contributing Code ## 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 for setup instructions and contributor guidelines. And don't forget to sign the
[CLA](https://signal.org/cla/). [CLA](https://signal.org/cla/).

View file

@ -19,79 +19,65 @@
"message": "&Help", "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." "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": { "loading": {
"message": "Loading...", "message": "Loading...",
"description": "Message shown on the loading screen before we've loaded any messages" "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": { "chooseDirectory": {
"message": "Choose directory", "message": "Choose folder",
"description": "Button to allow the user to export all data from app as part of migration process" "description": "Button to allow the user to find a folder on disk"
}, },
"exportButton": { "loadDataHeader": {
"message": "Export", "message": "Load your data",
"desription": "Button shown on the choose directory dialog which starts the export process" "description": "Header shown on the first screen in the data import process"
}, },
"exportChooserTitle": { "loadDataDescription": {
"message": "Choose target directory for data", "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": "Title of the popup window used to select data storage location" "description": "Introduction to the process of importing messages and contacts from disk"
},
"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"
}, },
"importChooserTitle": { "importChooserTitle": {
"message": "Choose directory with exported data", "message": "Choose directory with exported data",
"description": "Title of the popup window used to select data previously exported" "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": { "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." "description": "Message shown if the import went wrong."
}, },
"tryAgain": { "importAgain": {
"message": "Try again", "message": "Choose folder and try again",
"description": "Button shown if the user runs into an error during import, allowing them to start over" "description": "Button shown if the user runs into an error during import, allowing them to start over"
}, },
"importInstructions": { "importCompleteHeader": {
"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.", "message": "Success!",
"description": "Description of the export process" "description": "Header shown on the screen at the end of a successful import process"
}, },
"importing": { "importCompleteStartButton": {
"message": "Please wait while we import your data...", "message": "Start using Signal Desktop",
"description": "Shown as we are loading the user's data from disk" "description": "Button shown at end of successful import process, nothing left but a restart"
}, },
"importComplete": { "importCompleteLinkButton": {
"message": "We've successfully loaded your data. The next step is to restart the application!", "message": "Link this device to your phone",
"description": "Shown when the import is complete." "description": "Button shown at end of successful 'light' import process, so the standard linking process still needs to happen"
}, },
"selectedLocation": { "selectedLocation": {
"message": "your selected location", "message": "your selected location",
@ -526,87 +512,46 @@
"message": "Privacy is possible. Signal makes it easy.", "message": "Privacy is possible. Signal makes it easy.",
"description": "Tagline displayed under 'installWelcome' string on the install page" "description": "Tagline displayed under 'installWelcome' string on the install page"
}, },
"installNew": { "linkYourPhone": {
"message": "Set up as new install", "message": "Link your phone to Signal Desktop",
"description": "One of two choices presented on the screen shown on first launch" "description": "Shown on the front page when the application first starst, above the QR code"
}, },
"installImport": { "signalSettings": {
"message": "Set up with exported data", "message": "Signal Settings",
"description": "One of two choices presented on the screen shown on first launch" "description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
}, },
"installGetStartedButton": { "linkedDevices": {
"message": "Get started" "message": "Linked Devices",
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
}, },
"installSignalLink": { "plusButton": {
"message": "First, install <a $a_params$>Signal</a> on your mobile phone. We'll link your devices and keep your messages in sync.", "message": "'+' Button",
"description": "Prompt the user to install Signal on their phone before linking", "description": "The button used in Signal Android to add a new linked device"
"placeholders": {
"a_params": {
"content": "$1",
"example": "href='http://example.com'"
}
}
}, },
"installSignalLinks": { "linkNewDevice": {
"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.", "message": "Link New Device",
"description": "Prompt the user to install Signal on their phone before linking", "description": "The menu option shown in Signal iOS to add a new linked device"
"placeholders": {
"play_store": {
"content": "$1",
"example": "href='http://example.com'"
},
"app_store": {
"content": "$2",
"example": "href='http://example.com'"
}
}
}, },
"installGotIt": { "deviceName": {
"message": "Got it", "message": "Device name",
"description": "Button for the user to confirm that they have Signal installed." "description": "The label in settings panel shown for the user-provided name for this desktop instance"
}, },
"installIHaveSignalButton": { "chooseDeviceName": {
"message": "I have Signal for Android", "message": "Choose this device's name",
"description": "Button for the user to confirm that they have Signal for Android" "description": "The header shown on the 'choose device name' screen in the device linking process"
}, },
"installFollowUs": { "finishLinkingPhone": {
"message": "<a $a_params$>Follow us</a> for updates about multi-device support for iOS.", "message": "Finish linking phone",
"placeholders": { "description": "The text on the button to finish the linking process, after choosing the device name"
"a_params": {
"content": "$1",
"example": "href='http://example.com'"
}
}
}, },
"installAndroidInstructions": { "initialSync": {
"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." "message": "Syncing contacts and groups",
}, "description": "Shown during initial link while contacts and groups are being pulled from mobile device"
"installConnecting": {
"message": "Connecting...",
"description": "Displayed when waiting for the QR Code"
}, },
"installConnectionFailed": { "installConnectionFailed": {
"message": "Failed to connect to server.", "message": "Failed to connect to server.",
"description": "Displayed when we can't connect to the 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": { "installTooManyDevices": {
"message": "Sorry, you have too many devices linked already. Try removing some." "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": { "theyChangedTheTimer": {
"message": "$name$ set the timer to $time$.", "message": "$name$ set the timer to $time$.",
"description": "Message displayed when someone else changes the message expiration timer in a conversation.", "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." "message": "There is a new version of Signal available."
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart to apply the updates." "message": "Press Restart Signal to apply the updates."
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart" "message": "Restart Signal"
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {
"message": "Later" "message": "Later"

View file

@ -42,7 +42,7 @@ function showUpdateDialog(mainWindow, messages) {
if (response === RESTART_BUTTON) { if (response === RESTART_BUTTON) {
// We delay these update calls because they don't seem to work in this // 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. // 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(() => { setTimeout(() => {
windowState.markShouldQuit(); windowState.markShouldQuit();
autoUpdater.quitAndInstall(); autoUpdater.quitAndInstall();

View file

@ -2,8 +2,14 @@ const path = require('path');
const electronIsDev = require('electron-is-dev'); const electronIsDev = require('electron-is-dev');
const defaultEnvironment = electronIsDev ? 'development' : 'production'; let environment;
const environment = process.env.NODE_ENV || defaultEnvironment;
// 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 // Set environment vars to configure node-config before requiring it
process.env.NODE_ENV = environment; 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 path = require('path');
const fs = require('fs'); const fs = require('fs');
@ -117,8 +120,8 @@ function eliminateOutOfDateFiles(logPath, date) {
const file = { const file = {
path: target, path: target,
start: isLineAfterDate(start, date), start: isLineAfterDate(start, date),
end: isLineAfterDate(end[end.length - 1], date) end: isLineAfterDate(end[end.length - 1], date) ||
|| isLineAfterDate(end[end.length - 2], date), isLineAfterDate(end[end.length - 2], date),
}; };
if (!file.start && !file.end) { if (!file.start && !file.end) {

View file

@ -6,6 +6,9 @@ function createTemplate(options, messages) {
openNewBugForm, openNewBugForm,
openSupportPage, openSupportPage,
openForums, openForums,
setupWithImport,
setupAsNewDevice,
setupAsStandalone,
} = options; } = options;
const template = [{ 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') { if (process.platform === 'darwin') {
return updateForMac(template, messages, options); return updateForMac(template, messages, options);
} }
@ -134,14 +158,46 @@ function updateForMac(template, messages, options) {
const { const {
showWindow, showWindow,
showAbout, showAbout,
setupWithImport,
setupAsNewDevice,
setupAsStandalone,
} = options; } = options;
// Remove About item and separator from Help menu, since it's on the first menu // Remove About item and separator from Help menu, since it's on the first menu
template[4].submenu.pop(); template[4].submenu.pop();
template[4].submenu.pop(); template[4].submenu.pop();
// Replace File menu // Remove File menu
template.shift(); 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({ template.unshift({
submenu: [ submenu: [
{ {
@ -170,7 +226,8 @@ function updateForMac(template, messages, options) {
}); });
// Add to Edit menu // Add to Edit menu
template[1].submenu.push( const editIndex = options.includeSetup ? 2 : 1;
template[editIndex].submenu.push(
{ {
type: 'separator', type: 'separator',
}, },

View file

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

View file

@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
content="default-src 'none'; content="default-src 'none';
connect-src 'self' wss: https:; connect-src 'self' https: wss:;
script-src 'self'; script-src 'self';
style-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:; img-src 'self' blob: data:;
@ -536,7 +536,7 @@
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='link_to_support'> <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 }} {{ learnMore }}
</a> </a>
</script> </script>
@ -562,7 +562,7 @@
</div> </div>
<p> <p>
<a class='report-link' target='_blank' <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 }} {{ reportIssue }}
</a> </a>
</p> </p>
@ -571,6 +571,9 @@
<div class='content'> <div class='content'>
<a class='x close' alt='close settings' href='#'></a> <a class='x close' alt='close settings' href='#'></a>
<h2>{{ settings }}</h2> <h2>{{ settings }}</h2>
<div class='device-name-settings'>
<b>{{ deviceNameLabel }}:</b> {{ deviceName }}
</div>
<hr> <hr>
<div class='theme-settings'> <div class='theme-settings'>
<h3>{{ theme }}</h3> <h3>{{ theme }}</h3>
@ -652,151 +655,198 @@
{{/action }} {{/action }}
</script> </script>
<script type='text/x-tmpl-mustache' id='install-choice'> <script type='text/x-tmpl-mustache' id='import-flow-template'>
<div class='step'> {{#isStep2}}
<div id='step2' class='step'>
<div class='inner'> <div class='inner'>
<div class='step-body'> <div class='step-body'>
<img id='signal-icon' src='images/icon_250.png'/> <span class='banner-icon folder-outline'></span>
<h1>{{ installWelcome }}</h1> <div class='header'>{{ chooseHeader }}</div>
<p>{{ installTagline }}</p> <div class='body-text'>{{ choose }}</div>
</div> </div>
<div class='nav'> <div class='nav'>
<div> <a class='button new'>{{ installNew }}</a> </div> <div>
<div> <a class='button import'>{{ installImport }}</a> </div> <a class='button choose'>{{ chooseButton }}</a>
</div>
</div> </div>
</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>
<script type='text/x-tmpl-mustache' id='install_flow_template'> <script type='text/x-tmpl-mustache' id='link-flow-template'>
{{#isStep3}}
<div id='step2' class='step hidden'> <div id='step3' class='step'>
<div class='inner'> <div class='inner'>
<div class='step-body'> <div class='step-body'>
<img id='signal-phone' src='images/signal-phone.png'> <div class='header'>{{ linkYourPhone }}</div>
<p>{{{ installSignalLink }}}</p> <div id="qr">
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
</div>
</div> </div>
<div class='nav'> <div class='nav'>
<div> <a class='button step3'>{{ installIHaveSignalButton }}</a> </div> <div class='instructions'>
<div class='dot-container'> <div class='android'>
<span class='dot step1'></span> <div class='label'>
<span class='dot step2 selected'></span> <span class='os-icon android'></span>
<span class='dot step3'></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> </div>
</div> </div>
{{/isStep3}}
<div id='step3' class='step hidden'> {{#isStep4}}
<div id='step4' class='step'>
<div class='inner'> <div class='inner'>
<div class='step-body'> <div class='step-body'>
<div id="qr"></div> <span class='banner-icon lead-pencil'></span>
<p>{{ installAndroidInstructions }}</p> <div class='header'>{{ chooseName }}</div>
</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>
<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> </div>
<div class='nav'> <div class='nav'>
<div> <div>
<input type='submit' class='button' id='sync' value='{{ installFinalButton }}' /> <a class='button finish'>{{ finishLinkingPhoneButton }}</a>
</div> </div>
</div> </div>
</div> </div>
</form> </div>
{{/isStep4}}
<div id='step5' class='step hidden'> {{#isStep5}}
<div id='step5' class='step'>
<div class='inner'> <div class='inner'>
<div class='step-body'> <div class='step-body'>
<img id='signal-icon' src='images/icon_250.png'/> <span class='banner-icon sync'></span>
<div class='progress-dialog'> <div class='header'>{{ syncing }}</div>
<p class='status'></p> </div>
<div class='bar-container'><div class='bar progress-bar'></div></div> <div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
</div> </div>
</div> </div>
<div class='nav'>
</div>
</div> </div>
</div> </div>
{{/isStep5}}
<div id='stepTooManyDevices' class='step hidden'> {{#isError}}
<div class='inner error-dialog clearfix'> <div id='error' class='step'>
<div class='panel step-body'>{{ installTooManyDevices }}</div> <div class='inner'>
<div class='nav'> <div class='step-body'>
<button class='ok step3'>{{ ok }}</button> <span class='banner-icon alert-outline'></span>
</div> <div class='header'>{{ errorHeader }}</div>
</div> <div class='body'>{{ errorMessage }}</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>
<div class='nav'> <div class='nav'>
<a class='button try-again'>{{ errorButton }}</a>
</div> </div>
</div> </div>
</div> </div>
{{/isError}}
</script> </script>
<script type='text/x-tmpl-mustache' id='standalone'> <script type='text/x-tmpl-mustache' id='standalone'>
<header> <div class='step'>
<div class='container'> <div class='inner'>
<div class='row'> <div class='step-body'>
<div class='col-xs-2 col-md-1'> <img class='banner-image' src='images/icon_128.png' />
<img id='textsecure-icon' src='images/icon_250.png'/> <div class='header'>Create your Signal Account</div>
</div> <div id='phone-number-input'>
<div class='col-xs-10 col-md-11'> <div class='phone-input-form'>
<h1>Create your Signal Account</h1> <div id='number-container' class='number-container'>
<h4 class='tagline'>Private messaging from your web browser.</h4> <input type='tel' class='number' placeholder='Phone Number' />
</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> </div>
</div> </div>
</div> </div>
<div class='clearfix'> <div class='clearfix'>
<button id="request-sms" class="btn btn-info">Send SMS</button> <a class='button' id='request-sms'>Send SMS</a>
<button id="request-voice" class="btn btn-info" tabindex=-1>Call</button> <a class='link' id='request-voice' tabindex=-1>Call</a>
</div> </div>
<form id='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'>
<h2></h2> <div id='error' class='collapse'></div>
<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>
<div id=status></div> <div id=status></div>
</div> </div>
<div class='nav'>
<a class='button' id='verifyCode' data-loading-text='Please wait...'>Register</a>
</div>
</div> </div>
</div> </div>
</script> </script>
<script type='text/javascript' src='js/components.js'></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/reliable_trigger.js'></script>
<script type='text/javascript' src='js/database.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/phone-input-view.js"></script>
<script type='text/javascript' src='js/views/standalone_registration_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/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/views/import_view.js'></script>
<script type='text/javascript' src='js/wall_clock_listener.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", "version": "0.0.0",
"homepage": "https://github.com/WhisperSystems/TextSecure-Browser", "homepage": "https://github.com/signalapp/Signal-Desktop",
"license": "GPLV3", "license": "GPLV3",
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -15,7 +15,6 @@
"indexeddb-backbonejs-adapter": "*", "indexeddb-backbonejs-adapter": "*",
"intl-tel-input": "~4.0.1", "intl-tel-input": "~4.0.1",
"blueimp-load-image": "~1.13.0", "blueimp-load-image": "~1.13.0",
"blueimp-canvas-to-blob": "~2.1.1",
"autosize": "~4.0.0", "autosize": "~4.0.0",
"webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git", "webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git",
"mp3lameencoder": "https://github.com/higuma/mp3-lame-encoder-js.git", "mp3lameencoder": "https://github.com/higuma/mp3-lame-encoder-js.git",
@ -69,12 +68,6 @@
"build/img/flags.png", "build/img/flags.png",
"build/js/intlTelInput.js" "build/js/intlTelInput.js"
], ],
"blueimp-load-image": [
"js/load-image.js"
],
"blueimp-canvas-to-blob": [
"js/canvas-to-blob.js"
],
"emojijs": [ "emojijs": [
"lib/emoji.js", "lib/emoji.js",
"demo/emoji.css" "demo/emoji.css"
@ -113,8 +106,6 @@
"moment", "moment",
"intl-tel-input", "intl-tel-input",
"backbone.typeahead", "backbone.typeahead",
"blueimp-load-image",
"blueimp-canvas-to-blob",
"autosize", "autosize",
"filesize" "filesize"
], ],

View file

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

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 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 * Export all data from an IndexedDB database
* @param {IDBDatabase} idb_db * @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) { return new Promise(function(resolve, reject) {
var storeNames = idb_db.objectStoreNames; var storeNames = idb_db.objectStoreNames;
storeNames = _.without(storeNames, 'messages'); 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 = []; var exportedStoreNames = [];
if (storeNames.length === 0) { if (storeNames.length === 0) {
throw new Error('No stores to export'); throw new Error('No stores to export');
@ -160,9 +177,10 @@
}); });
} }
function importNonMessages(idb_db, parent) { function importNonMessages(idb_db, parent, options) {
return readFileAsText(parent, 'db.json').then(function(string) { var file = 'db.json';
return importFromJsonString(idb_db, string); 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)); 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 * Import data from JSON into an IndexedDB database. This does not delete any existing data
* from the database, so keys could clash * from the database, so keys could clash
@ -183,19 +211,50 @@
* @param {IDBDatabase} idb_db * @param {IDBDatabase} idb_db
* @param {string} jsonString - data to import, one key per object store * @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) { return new Promise(function(resolve, reject) {
var importObject = JSON.parse(jsonString); var importObject = JSON.parse(jsonString);
delete importObject.debug; 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(', ')); console.log('Importing to these stores:', storeNames.join(', '));
var finished = false; var finished = false;
var finish = function(via) { var finish = function(via) {
console.log('non-messages import done via', via); console.log('non-messages import done via', via);
if (finished) { if (finished) {
resolve(); resolve(result);
} }
finished = true; finished = true;
}; };
@ -219,20 +278,46 @@
} }
var count = 0; 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) { _.each(importObject[storeName], function(toAdd) {
toAdd = unstringify(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); var request = transaction.objectStore(storeName).put(toAdd, toAdd.id);
request.onsuccess = function(event) { request.onsuccess = function(event) {
count++; count++;
if (count == importObject[storeName].length) { if (count == importObject[storeName].length) {
// added all objects for this store finishStore();
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');
}
} }
}; };
request.onerror = function(e) { 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) { request.onsuccess = function(event) {
var cursor = event.target.result; var cursor = event.target.result;
if (cursor) { if (cursor) {
if (count !== 0) {
stream.write(',');
}
var message = cursor.value; var message = cursor.value;
var messageId = message.received_at; var messageId = message.received_at;
var attachments = message.attachments; 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) { message.attachments = _.map(attachments, function(attachment) {
return _.omit(attachment, ['data']); return _.omit(attachment, ['data']);
}); });
@ -598,6 +695,10 @@
})); }));
} }
function saveMessage(idb_db, message) {
return saveAllMessages(idb_db, [message]);
}
function saveAllMessages(idb_db, messages) { function saveAllMessages(idb_db, messages) {
if (!messages.length) { if (!messages.length) {
return Promise.resolve(); return Promise.resolve();
@ -658,43 +759,64 @@
// message, save it, and only then do we move on to the next message. Thus, every // 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 // message with attachments needs to be removed from our overall message save with the
// filter() call. // 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) { return readFileAsText(dir, 'messages.json').then(function(contents) {
var promiseChain = Promise.resolve(); var promiseChain = Promise.resolve();
var json = JSON.parse(contents); var json = JSON.parse(contents);
var conversationId;
if (json.messages && json.messages.length) { 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) { var messages = _.filter(json.messages, function(message) {
message = unstringify(message); message = unstringify(message);
if (messageLookup[getMessageKey(message)]) {
skipped++;
return false;
}
if (message.attachments && message.attachments.length) { if (message.attachments && message.attachments.length) {
var process = function() { var process = function() {
return loadAttachments(dir, message).then(function() { return loadAttachments(dir, message).then(function() {
return saveAllMessages(idb_db, [message]); return saveMessage(idb_db, message);
}); });
}; };
promiseChain = promiseChain.then(process); 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() { .then(function() {
return promiseChain; return promiseChain;
}) })
.then(function() { .then(function() {
console.log( console.log(
'Finished importing conversation', 'Finished importing conversation',
// Don't know if group or private conversation, so we blindly redact conversationId,
conversationId ? '[REDACTED]' + conversationId.slice(-3) : 'with no messages' '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) { return getDirContents(dir).then(function(contents) {
var promiseChain = Promise.resolve(); var promiseChain = Promise.resolve();
@ -713,7 +835,7 @@
} }
var process = function() { var process = function() {
return importConversation(idb_db, conversationDir); return importConversation(idb_db, conversationDir, options);
}; };
promiseChain = promiseChain.then(process); 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) { function clearAllStores(idb_db) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
console.log('Clearing all indexeddb stores'); console.log('Clearing all indexeddb stores');
@ -791,7 +980,7 @@
}; };
return getDirectory(options); return getDirectory(options);
}, },
backupToDirectory: function(directory) { exportToDirectory: function(directory, options) {
var dir; var dir;
var idb; var idb;
return openDatabase().then(function(idb_db) { return openDatabase().then(function(idb_db) {
@ -800,7 +989,7 @@
return createDirectory(directory, name); return createDirectory(directory, name);
}).then(function(created) { }).then(function(created) {
dir = created; dir = created;
return exportNonMessages(idb, dir); return exportNonMessages(idb, dir, options);
}).then(function() { }).then(function() {
return exportConversations(idb, dir); return exportConversations(idb, dir);
}).then(function() { }).then(function() {
@ -823,18 +1012,30 @@
}; };
return getDirectory(options); return getDirectory(options);
}, },
importFromDirectory: function(directory) { importFromDirectory: function(directory, options) {
var idb; options = options || {};
var idb, nonMessageResult;
return openDatabase().then(function(idb_db) { return openDatabase().then(function(idb_db) {
idb = 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() { }).then(function() {
return importConversations(idb, directory); return importNonMessages(idb, directory, options);
}).then(function(result) {
nonMessageResult = result;
return importConversations(idb, directory, options);
}).then(function() { }).then(function() {
return directory;
}).then(function(path) {
console.log('done restoring from backup!'); console.log('done restoring from backup!');
return path; return nonMessageResult;
}, function(error) { }, function(error) {
console.log( console.log(
'the import went wrong:', 'the import went wrong:',

View file

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

View file

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

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) { if (ab1.byteLength !== ab2.byteLength) {
return false; return false;
} }
var result = true; var result = 0;
var ta1 = new Uint8Array(ab1); var ta1 = new Uint8Array(ab1);
var ta2 = new Uint8Array(ab2); var ta2 = new Uint8Array(ab2);
for (var i = 0; i < ab1.byteLength; ++i) { 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 }); var Model = Backbone.Model.extend({ database: Whisper.Database });
@ -688,7 +688,7 @@
}.bind(this)); }.bind(this));
}, },
// This matches the Java method as of // 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) { processVerifiedMessage: function(identifier, verifiedStatus, publicKey) {
if (identifier === null || identifier === undefined) { if (identifier === null || identifier === undefined) {
throw new Error("Tried to set verified for undefined/null key"); throw new Error("Tried to set verified for undefined/null key");

View file

@ -7,12 +7,12 @@
initialize: function(options) { initialize: function(options) {
this.inboxView = null; this.inboxView = null;
this.installView = null; this.installView = null;
this.applyTheme(); this.applyTheme();
this.applyHideMenu(); this.applyHideMenu();
}, },
events: { events: {
'click .openInstaller': 'openInstaller', 'click .openInstaller': 'openInstaller', // NetworkStatusView has this button
'click .openStandalone': 'openStandalone',
'openInbox': 'openInbox', 'openInbox': 'openInbox',
'change-theme': 'applyTheme', 'change-theme': 'applyTheme',
'change-hide-menu': 'applyHideMenu', 'change-hide-menu': 'applyHideMenu',
@ -45,39 +45,38 @@
this.debugLogView = null; 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() { openImporter: function() {
this.closeImporter(); window.addSetupMenuItems();
this.closeInstallChoice(); this.resetViews();
var importView = this.importView = new Whisper.ImportView(); 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); this.openView(this.importView);
}, },
finishLightImport: function() {
var options = {
hasExistingData: true
};
this.openInstaller(options);
},
closeImporter: function() { closeImporter: function() {
if (this.importView) { if (this.importView) {
this.importView.remove(); this.importView.remove();
this.importView = null; this.importView = null;
} }
}, },
openInstaller: function() { openInstaller: function(options) {
this.closeInstaller(); options = options || {};
this.closeInstallChoice();
var installView = this.installView = new Whisper.InstallView(); // If we're in the middle of import, we don't want to show the menu options
this.listenTo(installView, 'cancel', this.openInstallChoice.bind(this)); // 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); this.openView(this.installView);
}, },
closeInstaller: function() { closeInstaller: function() {
@ -88,11 +87,23 @@
}, },
openStandalone: function() { openStandalone: function() {
if (window.config.environment !== 'production') { if (window.config.environment !== 'production') {
this.closeInstaller(); window.addSetupMenuItems();
this.installView = new Whisper.StandaloneRegistrationView(); this.resetViews();
this.openView(this.installView); 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) { openInbox: function(options) {
options = options || {}; options = options || {};
// The inbox can be created before the 'empty' event fires or afterwards. If // The inbox can be created before the 'empty' event fires or afterwards. If

View file

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

View file

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

View file

@ -7,7 +7,8 @@
var State = { var State = {
IMPORTING: 1, IMPORTING: 1,
COMPLETE: 2 COMPLETE: 2,
LIGHT_COMPLETE: 3,
}; };
var IMPORT_STARTED = 'importStarted'; var IMPORT_STARTED = 'importStarted';
@ -39,12 +40,13 @@
}; };
Whisper.ImportView = Whisper.View.extend({ Whisper.ImportView = Whisper.View.extend({
templateName: 'app-migration-screen', templateName: 'import-flow-template',
className: 'app-loading-screen', className: 'full-screen-flow',
events: { events: {
'click .import': 'onImport', 'click .choose': 'onImport',
'click .restart': 'onRestart', 'click .restart': 'onRestart',
'click .cancel': 'onCancel', 'click .cancel': 'onCancel',
'click .register': 'onRegister',
}, },
initialize: function() { initialize: function() {
if (Whisper.Import.isIncomplete()) { if (Whisper.Import.isIncomplete()) {
@ -55,41 +57,42 @@
this.pending = Promise.resolve(); this.pending = Promise.resolve();
}, },
render_attributes: function() { render_attributes: function() {
var message;
var importButton;
var hideProgress = true;
var restartButton;
var cancelButton;
if (this.error) { if (this.error) {
return { return {
message: i18n('importError'), isError: true,
hideProgress: true, errorHeader: i18n('importErrorHeader'),
importButton: i18n('tryAgain'), errorMessage: i18n('importError'),
chooseButton: i18n('importAgain'),
}; };
} }
switch (this.state) { var restartButton = i18n('importCompleteStartButton');
case State.COMPLETE: var registerButton = i18n('importCompleteLinkButton');
message = i18n('importComplete'); var step = 'step2';
restartButton = i18n('restartSignal');
break; if (this.state === State.IMPORTING) {
case State.IMPORTING: step = 'step3';
message = i18n('importing'); } else if (this.state === State.COMPLETE) {
hideProgress = false; registerButton = null;
break; step = 'step4';
default: } else if (this.state === State.LIGHT_COMPLETE) {
message = i18n('importInstructions'); restartButton = null;
importButton = i18n('chooseDirectory'); step = 'step4';
cancelButton = i18n('cancel');
} }
return { return {
hideProgress: hideProgress, isStep2: step === 'step2',
message: message, chooseHeader: i18n('loadDataHeader'),
importButton: importButton, choose: i18n('loadDataDescription'),
chooseButton: i18n('chooseDirectory'),
isStep3: step === 'step3',
importingHeader: i18n('importingHeader'),
isStep4: step === 'step4',
completeHeader: i18n('importCompleteHeader'),
restartButton: restartButton, restartButton: restartButton,
cancelButton: cancelButton, registerButton: registerButton,
}; };
}, },
onRestart: function() { onRestart: function() {
@ -110,9 +113,16 @@
} }
}); });
}, },
doImport: function(directory) { onRegister: function() {
this.error = null; // 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.state = State.IMPORTING;
this.render(); this.render();
@ -125,25 +135,17 @@
Whisper.Import.start(), Whisper.Import.start(),
Whisper.Backup.importFromDirectory(directory) Whisper.Backup.importFromDirectory(directory)
]); ]);
}).then(function() { }).then(function(results) {
// Catching in-memory cache up with what's in indexeddb now... var importResult = results[1];
// 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), // A full import changes so much we need a restart of the app
Whisper.Import.complete() if (importResult.fullImport) {
]); return this.finishFullImport(directory);
}).then(function() { }
this.state = State.COMPLETE;
this.render(); // 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) { }.bind(this)).catch(function(error) {
console.log('Error importing:', error && error.stack ? error.stack : error); console.log('Error importing:', error && error.stack ? error.stack : error);
@ -153,6 +155,40 @@
return Whisper.Backup.clearDatabase(); return Whisper.Backup.clearDatabase();
}.bind(this)); }.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', NETWORK_ERROR: 'NetworkError',
}; };
var DEVICE_NAME_SELECTOR = 'input.device-name';
var CONNECTION_ERROR = -1;
var TOO_MANY_DEVICES = 411;
Whisper.InstallView = Whisper.View.extend({ Whisper.InstallView = Whisper.View.extend({
templateName: 'install_flow_template', templateName: 'link-flow-template',
className: 'main install', className: 'main full-screen-flow',
render_attributes: function() { events: {
var twitterHref = 'https://twitter.com/whispersystems'; 'click .try-again': 'connect',
var signalHref = 'https://signal.org/install'; // handler for finish button is in confirmNumber()
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'
};
}, },
initialize: function(options) { initialize: function(options) {
this.counter = 0; options = options || {};
this.render(); this.selectStep(Steps.SCAN_QR_CODE);
var deviceName = textsecure.storage.user.getDeviceName();
if (!deviceName) {
deviceName = window.config.hostname;
}
this.$('#device-name').val(deviceName);
this.selectStep(Steps.INSTALL_SIGNAL);
this.connect(); this.connect();
this.on('disconnected', this.reconnect); this.on('disconnected', this.reconnect);
if (Whisper.Registration.everDone()) { // Keep data around if it's a re-link, or the middle of a light import
this.selectStep(Steps.SCAN_QR_CODE); this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData;
this.hideDots(); },
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() { connect: function() {
this.error = null;
this.selectStep(Steps.SCAN_QR_CODE);
this.clearQR(); this.clearQR();
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
var accountManager = getAccountManager(); var accountManager = getAccountManager();
accountManager.registerSecondDevice( accountManager.registerSecondDevice(
this.setProvisioningUrl.bind(this), this.setProvisioningUrl.bind(this),
this.confirmNumber.bind(this), this.confirmNumber.bind(this)
this.incrementCounter.bind(this)
).catch(this.handleDisconnect.bind(this)); ).catch(this.handleDisconnect.bind(this));
}, },
handleDisconnect: function(e) { handleDisconnect: function(e) {
if (this.canceled) {
return;
}
console.log('provisioning failed', e.stack); console.log('provisioning failed', e.stack);
this.error = e;
this.render();
if (e.message === 'websocket closed') { if (e.message === 'websocket closed') {
this.showConnectionError();
this.trigger('disconnected'); this.trigger('disconnected');
} else if (e.name === 'HTTPError' && e.code == -1) { } else if (e.name !== 'HTTPError'
this.selectStep(Steps.NETWORK_ERROR); || (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) {
} else if (e.name === 'HTTPError' && e.code == 411) {
this.showTooManyDevices();
} else {
throw e; throw e;
} }
}, },
reconnect: function() { reconnect: function() {
setTimeout(this.connect.bind(this), 10000); if (this.timeout) {
}, clearTimeout(this.timeout);
events: function() { this.timeout = null;
return { }
'click .error-dialog .ok': 'connect', this.timeout = setTimeout(this.connect.bind(this), 10000);
'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');
}, },
clearQR: function() { 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) { setProvisioningUrl: function(url) {
this.$('#qr').html(''); if ($('#qr').length === 0) {
new QRCode(this.$('#qr')[0]).makeCode(url); 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) { confirmNumber: function(number) {
var parsed = libphonenumber.parse(number); window.removeSetupMenuItems();
var stepId = '#step' + Steps.ENTER_NAME;
this.$(stepId + ' .number').text(libphonenumber.format(
parsed,
libphonenumber.PhoneNumberFormat.INTERNATIONAL
));
this.selectStep(Steps.ENTER_NAME); this.selectStep(Steps.ENTER_NAME);
this.$('#device-name').focus(); this.setDeviceNameDefault();
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
this.$(stepId + ' .cancel').click(function(e) { this.$('.finish').click(function(e) {
reject();
});
this.$(stepId).submit(function(e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
var name = this.$('#device-name').val();
var name = this.$(DEVICE_NAME_SELECTOR).val();
name = name.replace(/\0/g,''); // strip unicode null name = name.replace(/\0/g,''); // strip unicode null
if (name.trim().length === 0) { if (name.trim().length === 0) {
this.$('#device-name').focus(); this.$(DEVICE_NAME_SELECTOR).focus();
return; return;
} }
this.$('.progress-dialog .status').text(i18n('installGeneratingKeys'));
this.selectStep(Steps.PROGRESS_BAR); 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));
}.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() { render_attributes: function() {
var seconds = this.model.get('expirationTimerUpdate').expireTimer; var seconds = this.model.get('expirationTimerUpdate').expireTimer;
var timerMessage; var timerMessage;
if (this.conversation.id === textsecure.storage.user.getNumber()) {
timerMessage = i18n('youChangedTheTimer', var timerUpdate = this.model.get('expirationTimerUpdate');
Whisper.ExpirationTimerOptions.getName(seconds)); 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 { } else {
timerMessage = i18n('theyChangedTheTimer', [ timerMessage = i18n('theyChangedTheTimer', [
this.conversation.getTitle(), this.conversation.getTitle(),
Whisper.ExpirationTimerOptions.getName(seconds)]); prettySeconds,
]);
} }
return { content: timerMessage }; return { content: timerMessage };
} }

View file

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

View file

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

View file

@ -35291,8 +35291,6 @@ var Internal = Internal || {};
result = result | (a[i] ^ b[i]); result = result | (a[i] ^ b[i]);
} }
if (result !== 0) { 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"); throw new Error("Bad MAC");
} }
}); });
@ -36016,14 +36014,7 @@ libsignal.SessionBuilder = function (storage, remoteAddress) {
this.processV3 = builder.processV3.bind(builder); this.processV3 = builder.processV3.bind(builder);
}; };
function SessionCipher(storage, remoteAddress, options) { function SessionCipher(storage, remoteAddress) {
options = options || {};
if (typeof options.messageKeysLimit === 'undefined') {
options.messageKeysLimit = 1000;
}
this.messageKeysLimit = options.messageKeysLimit;
this.remoteAddress = remoteAddress; this.remoteAddress = remoteAddress;
this.storage = storage; this.storage = storage;
} }
@ -36296,15 +36287,14 @@ SessionCipher.prototype = {
}); });
}, },
fillMessageKeys: function(chain, counter) { 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) { if (chain.chainKey.counter >= counter) {
return Promise.resolve(); // Already calculated 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) { if (chain.chainKey.key === undefined) {
throw new Error("Got invalid request to extend chain after it was already closed"); 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) { libsignal.SessionCipher = function(storage, remoteAddress) {
var cipher = new SessionCipher(storage, remoteAddress, options); var cipher = new SessionCipher(storage, remoteAddress);
// returns a Promise that resolves to a ciphertext object // returns a Promise that resolves to a ciphertext object
this.encrypt = cipher.encrypt.bind(cipher); this.encrypt = cipher.encrypt.bind(cipher);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,6 +42,26 @@
Whisper.events.trigger('showDebugLog'); 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 // We pull these dependencies in now, from here, because they have Node.js dependencies
require('./js/logging'); require('./js/logging');
@ -60,6 +80,8 @@
window.nodeSetImmediate(function() {}); window.nodeSetImmediate(function() {});
}, 1000); }, 1000);
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
window.loadImage = require('blueimp-load-image');
window.ProxyAgent = require('proxy-agent'); window.ProxyAgent = require('proxy-agent');
window.EmojiConvertor = require('emoji-js'); window.EmojiConvertor = require('emoji-js');
window.emojiData = require('emoji-datasource'); window.emojiData = require('emoji-datasource');
@ -70,6 +92,16 @@
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat; window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
window.nodeNotifier = require('node-notifier'); window.nodeNotifier = require('node-notifier');
const { autoOrientImage } = require('./js/modules/auto_orient_image');
window.autoOrientImage = autoOrientImage;
// ES2015+ modules
window.Signal = window.Signal || {};
window.Signal.Types = window.Signal.Types || {};
window.Signal.Types.Attachment = require('./js/modules/types/attachment');
window.Signal.Types.Message = require('./js/modules/types/message');
window.Signal.Types.MIME = require('./js/modules/types/mime');
// We pull this in last, because the native module involved appears to be sensitive to // We pull this in last, because the native module involved appears to be sensitive to
// /tmp mounted as noexec on Linux. // /tmp mounted as noexec on Linux.
require('./js/spell_check'); require('./js/spell_check');

View file

@ -17,7 +17,7 @@ if (!beta.test(version)) {
process.exit(); 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 { message ProvisioningUuid {
optional string uuid = 1; 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_package = "org.whispersystems.signalservice.internal.push";
option java_outer_classname = "TextSecureProtos"; option java_outer_classname = "SignalServiceProtos";
message Envelope { message Envelope {
enum Type { enum Type {
@ -22,40 +23,13 @@ message Envelope {
} }
message Content { message Content {
optional DataMessage dataMessage = 1; optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2; optional SyncMessage syncMessage = 2;
optional CallMessage callMessage = 3; optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4; optional NullMessage nullMessage = 4;
optional ReceiptMessage receiptMessage = 5; 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 CallMessage {
message Offer { message Offer {
optional uint64 id = 1; optional uint64 id = 1;
@ -92,7 +66,7 @@ message CallMessage {
message DataMessage { message DataMessage {
enum Flags { enum Flags {
END_SESSION = 1; END_SESSION = 1;
EXPIRATION_TIMER_UPDATE = 2; EXPIRATION_TIMER_UPDATE = 2;
PROFILE_KEY_UPDATE = 4; PROFILE_KEY_UPDATE = 4;
} }
@ -103,6 +77,34 @@ message DataMessage {
optional uint32 flags = 4; optional uint32 flags = 4;
optional uint32 expireTimer = 5; optional uint32 expireTimer = 5;
optional bytes profileKey = 6; 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 { message SyncMessage {
@ -115,7 +117,7 @@ message SyncMessage {
message Contacts { message Contacts {
optional AttachmentPointer blob = 1; optional AttachmentPointer blob = 1;
optional bool isComplete = 2 [default = false]; optional bool complete = 2 [default = false];
} }
message Groups { message Groups {
@ -143,19 +145,19 @@ message SyncMessage {
optional uint64 timestamp = 2; optional uint64 timestamp = 2;
} }
message Settings { message Configuration {
optional bool readReceipts = 1; optional bool readReceipts = 1;
} }
optional Sent sent = 1; optional Sent sent = 1;
optional Contacts contacts = 2; optional Contacts contacts = 2;
optional Groups groups = 3; optional Groups groups = 3;
optional Request request = 4; optional Request request = 4;
repeated Read read = 5; repeated Read read = 5;
optional Blocked blocked = 6; optional Blocked blocked = 6;
optional Verified verified = 7; optional Verified verified = 7;
optional bytes padding = 8; optional Configuration configuration = 9;
optional Settings settings = 9; optional bytes padding = 8;
} }
message AttachmentPointer { message AttachmentPointer {
@ -171,6 +173,8 @@ message AttachmentPointer {
optional bytes digest = 6; optional bytes digest = 6;
optional string fileName = 7; optional string fileName = 7;
optional uint32 flags = 8; optional uint32 flags = 8;
optional uint32 width = 9;
optional uint32 height = 10;
} }
message GroupContext { message GroupContext {
@ -194,12 +198,14 @@ message ContactDetails {
optional uint32 length = 2; optional uint32 length = 2;
} }
optional string number = 1; optional string number = 1;
optional string name = 2; optional string name = 2;
optional Avatar avatar = 3; optional Avatar avatar = 3;
optional string color = 4; optional string color = 4;
optional Verified verified = 5; optional Verified verified = 5;
optional bytes profileKey = 6; optional bytes profileKey = 6;
optional bool blocked = 7;
optional uint32 expireTimer = 8;
} }
message GroupDetails { message GroupDetails {
@ -208,9 +214,10 @@ message GroupDetails {
optional uint32 length = 2; optional uint32 length = 2;
} }
optional bytes id = 1; optional bytes id = 1;
optional string name = 2; optional string name = 2;
repeated string members = 3; repeated string members = 3;
optional Avatar avatar = 4; optional Avatar avatar = 4;
optional bool active = 5 [default = true]; 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 * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package textsecure; package signalservice;
option java_package = "org.whispersystems.websocket.messages.protobuf"; option java_package = "org.whispersystems.websocket.messages.protobuf";
@ -42,4 +42,4 @@ message WebSocketMessage {
optional Type type = 1; optional Type type = 1;
optional WebSocketRequestMessage request = 2; optional WebSocketRequestMessage request = 2;
optional WebSocketResponseMessage response = 3; optional WebSocketResponseMessage response = 3;
} }

View file

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

View file

@ -218,7 +218,7 @@ button.hamburger {
} }
.dropoff { .dropoff {
outline: solid 1px #2090ea; outline: solid 1px $blue;
} }
$avatar-size: 44px; $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 //yellow border fix
.inbox:focus { .inbox:focus {
outline: none; outline: none;

View file

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

View file

@ -6,335 +6,6 @@
background: url("../images/flags.png"); 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 { .intl-tel-input .country-list {
text-align: left; text-align: left;
} }

View file

@ -450,7 +450,7 @@
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='link_to_support'> <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 }} {{ learnMore }}
</a> </a>
</script> </script>
@ -476,7 +476,7 @@
</div> </div>
<p> <p>
<a class='report-link' target='_blank' <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 }} {{ reportIssue }}
</a> </a>
</p> </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 fs = require('fs');
const path = require('path'); const path = require('path');

View file

@ -2,18 +2,33 @@
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab
*/ */
describe('LastSeenIndicatorView', function() { describe('LastSeenIndicatorView', function() {
// TODO: in electron branch, where we have access to real i18n, test rendered HTML
it('renders provided count', function() { it('renders provided count', function() {
var view = new Whisper.LastSeenIndicatorView({count: 10}); var view = new Whisper.LastSeenIndicatorView({count: 10});
assert.equal(view.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() { it('increments count', function() {
var view = new Whisper.LastSeenIndicatorView({count: 4}); var view = new Whisper.LastSeenIndicatorView({count: 4});
assert.equal(view.count, 4); assert.equal(view.count, 4);
view.render();
assert.match(view.$el.html(), /4 Unread Messages/);
view.increment(3); view.increment(3);
assert.equal(view.count, 7); 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 * vim: ts=4:sw=4:expandtab
*/ */
describe('ScrollDownButtonView', function() { 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() { it('renders with count = 0', function() {
var view = new Whisper.ScrollDownButtonView(); var view = new Whisper.ScrollDownButtonView();
view.render(); view.render();
assert.equal(view.count, 0); 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() { it('renders with count = 1', function() {
@ -16,7 +14,7 @@ describe('ScrollDownButtonView', function() {
view.render(); view.render();
assert.equal(view.count, 1); assert.equal(view.count, 1);
assert.match(view.$el.html(), /new-messages/); 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() { it('renders with count = 2', function() {
@ -25,7 +23,7 @@ describe('ScrollDownButtonView', function() {
assert.equal(view.count, 2); assert.equal(view.count, 2);
assert.match(view.$el.html(), /new-messages/); 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() { it('increments count and re-renders', function() {

View file

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