Promote 1.5.0-beta.1 (and a couple bugfixes) to production
14
.editorconfig
Normal file
|
@ -0,0 +1,14 @@
|
|||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{js/modules/**/*.js, test/modules/**/*.js}]
|
||||
indent_size = 2
|
|
@ -1,17 +1,23 @@
|
|||
build/**
|
||||
components/**
|
||||
coverage/**
|
||||
dist/**
|
||||
libtextsecure/**
|
||||
coverage/**
|
||||
|
||||
# these aren't ready yet, pulling files in one-by-one
|
||||
js/**
|
||||
test/**
|
||||
js/*.js
|
||||
js/models/**/*.js
|
||||
js/react/**/*.js
|
||||
js/views/**/*.js
|
||||
test/*.js
|
||||
test/models/*.js
|
||||
test/views/*.js
|
||||
/*.js
|
||||
|
||||
# ES2015+ files
|
||||
!js/background.js
|
||||
!js/models/conversations.js
|
||||
!js/views/file_input_view.js
|
||||
!js/views/attachment_view.js
|
||||
!main.js
|
||||
!prepare_build.js
|
||||
|
||||
# all of these files will be new
|
||||
!test/server/**/*.js
|
||||
|
||||
# all of app/ is included
|
||||
|
|
12
.eslintrc.js
|
@ -11,6 +11,10 @@ module.exports = {
|
|||
'airbnb-base',
|
||||
],
|
||||
|
||||
plugins: [
|
||||
'more',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'comma-dangle': ['error', {
|
||||
arrays: 'always-multiline',
|
||||
|
@ -21,7 +25,7 @@ module.exports = {
|
|||
}],
|
||||
|
||||
// putting params on their own line helps stay within line length limit
|
||||
'function-paren-newline': ['error', 'consistent'],
|
||||
'function-paren-newline': ['error', 'multiline'],
|
||||
|
||||
// 90 characters allows three+ side-by-side screens on a standard-size monitor
|
||||
'max-len': ['error', {
|
||||
|
@ -29,6 +33,9 @@ module.exports = {
|
|||
ignoreUrls: true,
|
||||
}],
|
||||
|
||||
// encourage consistent use of `async` / `await` instead of `then`
|
||||
'more/no-then': 'error',
|
||||
|
||||
// it helps readability to put public API at top,
|
||||
'no-use-before-define': 'off',
|
||||
|
||||
|
@ -37,5 +44,8 @@ module.exports = {
|
|||
|
||||
// though we have a logger, we still remap console to log to disk
|
||||
'no-console': 'off',
|
||||
|
||||
// consistently place operators at end of line except ternaries
|
||||
'operator-linebreak': 'error',
|
||||
}
|
||||
};
|
||||
|
|
2
.github/ISSUE_TEMPLATE.md
vendored
|
@ -59,7 +59,7 @@ Operating System:
|
|||
<!-- Instructions for finding your OS version are here: http://whatsmyos.com/ -->
|
||||
|
||||
Linked device version:
|
||||
<!-- Android: Settings -> Advanced, iOS: Settings -> About -->
|
||||
<!-- Android: Settings -> Advanced, iOS: Settings -> General -> About -->
|
||||
|
||||
|
||||
### Link to debug log
|
||||
|
|
|
@ -8,7 +8,7 @@ install:
|
|||
- yarn install
|
||||
script:
|
||||
- yarn run generate
|
||||
- yarn prepare-build
|
||||
- yarn prepare-beta-build
|
||||
- yarn eslint
|
||||
- yarn test-server
|
||||
- yarn lint
|
||||
|
|
|
@ -12,7 +12,7 @@ for it or creating a new one yourself. You can use also that issue as a place to
|
|||
your intentions and get feedback from the users most likely to appreciate your changes.
|
||||
|
||||
You're most likely to have your pull request accepted easily if it addresses bugs already
|
||||
in the [Next Steps project](https://github.com/WhisperSystems/Signal-Desktop/projects/1),
|
||||
in the [Next Steps project](https://github.com/signalapp/Signal-Desktop/projects/1),
|
||||
especially if they are near the top of the Backlog column. Those are what we'll be looking
|
||||
at next, so it would be a great help if you helped us out!
|
||||
|
||||
|
@ -24,7 +24,7 @@ ounce of prevention, as they say!](https://www.goodreads.com/quotes/247269-an-ou
|
|||
## Developer Setup
|
||||
|
||||
First, you'll need [Node.js](https://nodejs.org/) which matches our current version.
|
||||
You can check [`.nvmrc` in the `development` branch](https://github.com/WhisperSystems/Signal-Desktop/blob/development/.nvmrc) to see what the current version is. If you have [nvm](https://github.com/creationix/nvm)
|
||||
You can check [`.nvmrc` in the `development` branch](https://github.com/signalapp/Signal-Desktop/blob/development/.nvmrc) to see what the current version is. If you have [nvm](https://github.com/creationix/nvm)
|
||||
you can just run `nvm use` in the project directory and it will switch to the project's
|
||||
desired Node.js version. [nvm for windows](https://github.com/coreybutler/nvm-windows) is
|
||||
still useful, but it doesn't support `.nvmrc` files.
|
||||
|
@ -46,7 +46,7 @@ favorite package manager. Python 2.x and GCC are two key necessary components.
|
|||
Now, run these commands in your preferred terminal in a good directory for development:
|
||||
|
||||
```
|
||||
git clone https://github.com/WhisperSystems/Signal-Desktop.git
|
||||
git clone https://github.com/signalapp/Signal-Desktop.git
|
||||
cd Signal-Desktop
|
||||
npm install -g yarn # (only if you don't already have yarn)
|
||||
npm install -g grunt-cli # (only if you don't already have grunt)
|
||||
|
@ -215,13 +215,13 @@ and register it with one of your extra phone numbers:
|
|||
|
||||
First, build Signal for Android or iOS from source, and point its TextSecure service URL to `textsecure-service-staging.whispersystems.org`:
|
||||
|
||||
**on Android:** Replace the `SIGNAL_URL` value in [build.gradle](https://github.com/WhisperSystems/Signal-Android/blob/master/build.gradle)
|
||||
**on Android:** Replace the `SIGNAL_URL` value in [build.gradle](https://github.com/signalapp/Signal-Android/blob/master/build.gradle)
|
||||
|
||||
**on iOS:** Replace the `textSecureServerURL` value in `TSConstants.h`(located in the SignalServiceKit pod)
|
||||
|
||||
This task is 1% search and replace, 99% setting up your build environment. Instructions are available for both
|
||||
the [Android](https://github.com/WhisperSystems/Signal-Android/blob/master/BUILDING.md)
|
||||
and [iOS](https://github.com/WhisperSystems/Signal-iOS/blob/master/BUILDING.md) projects.
|
||||
the [Android](https://github.com/signalapp/Signal-Android/blob/master/BUILDING.md)
|
||||
and [iOS](https://github.com/signalapp/Signal-iOS/blob/master/BUILDING.md) projects.
|
||||
|
||||
Then you can set up your development build of Signal Desktop as normal. If you've already
|
||||
set up as a standalone install, you can switch by opening the DevTools (View -> Toggle
|
||||
|
|
|
@ -103,6 +103,7 @@ module.exports = function(grunt) {
|
|||
'!js/Mp3LameEncoder.min.js',
|
||||
'!js/libsignal-protocol-worker.js',
|
||||
'!js/components.js',
|
||||
'!js/modules/**/*.js',
|
||||
'!js/signal_protocol_store.js',
|
||||
'_locales/**/*'
|
||||
],
|
||||
|
@ -174,8 +175,10 @@ module.exports = function(grunt) {
|
|||
'!js/Mp3LameEncoder.min.js',
|
||||
'!js/libsignal-protocol-worker.js',
|
||||
'!js/components.js',
|
||||
'!js/modules/**/*.js',
|
||||
'test/**/*.js',
|
||||
'!test/blanket_mocha.js',
|
||||
'!test/modules/**/*.js',
|
||||
'!test/test.js',
|
||||
]
|
||||
}
|
||||
|
@ -282,7 +285,7 @@ module.exports = function(grunt) {
|
|||
var https = require('https');
|
||||
|
||||
var urlBase = "https://s3-us-west-1.amazonaws.com/signal-desktop-builds";
|
||||
var keyBase = 'WhisperSystems/Signal-Desktop';
|
||||
var keyBase = 'signalapp/Signal-Desktop';
|
||||
var sha = gitinfo.local.branch.current.SHA;
|
||||
var files = [{
|
||||
zip: packageJson.name + '-' + packageJson.version + '.zip',
|
||||
|
|
10
README.md
|
@ -1,10 +1,10 @@
|
|||
[](https://travis-ci.org/WhisperSystems/Signal-Desktop)
|
||||
[](https://travis-ci.org/signalapp/Signal-Desktop)
|
||||
Signal Desktop
|
||||
==========================
|
||||
|
||||
Signal Desktop is an Electron application that links with your
|
||||
[Signal Android](https://github.com/WhisperSystems/Signal-Android)
|
||||
or [Signal iOS](https://github.com/WhisperSystems/Signal-iOS) app.
|
||||
[Signal Android](https://github.com/signalapp/Signal-Android)
|
||||
or [Signal iOS](https://github.com/signalapp/Signal-iOS) app.
|
||||
|
||||
## Install production version: https://signal.org/download/
|
||||
|
||||
|
@ -27,7 +27,7 @@ The discussion groups are another good place for questions: https://whispersyste
|
|||
|
||||
Please search the existing issues for your bug and create a new one if the issue is not yet tracked!
|
||||
|
||||
https://github.com/WhisperSystems/Signal-Desktop/issues
|
||||
https://github.com/signalapp/Signal-Desktop/issues
|
||||
|
||||
|
||||
## Contributing Translations
|
||||
|
@ -38,7 +38,7 @@ https://www.transifex.com/projects/p/signal-desktop
|
|||
|
||||
## Contributing Code
|
||||
|
||||
Please see [CONTRIBUTING.md](https://github.com/WhisperSystems/Signal-Desktop/blob/master/CONTRIBUTING.md)
|
||||
Please see [CONTRIBUTING.md](https://github.com/signalapp/Signal-Desktop/blob/master/CONTRIBUTING.md)
|
||||
for setup instructions and contributor guidelines. And don't forget to sign the
|
||||
[CLA](https://signal.org/cla/).
|
||||
|
||||
|
|
|
@ -19,79 +19,65 @@
|
|||
"message": "&Help",
|
||||
"description": "The label that is used for the Help menu in the program main menu. The '&' indicates that the following letter will be used as the keyboard 'shortcut letter' for accessing the menu with the Alt-<letter> combination."
|
||||
},
|
||||
"menuSetupWithImport": {
|
||||
"message": "Set up with import",
|
||||
"description": "When the application is not yet set up, menu option to start up the import sequence"
|
||||
},
|
||||
"menuSetupAsNewDevice": {
|
||||
"message": "Set up as new device",
|
||||
"description": "When the application is not yet set up, menu option to start up the set up as fresh device"
|
||||
},
|
||||
"menuSetupAsStandalone": {
|
||||
"message": "Set up as standalone device",
|
||||
"description": "Only available on development modes, menu option to open up the standalone device setup sequence"
|
||||
},
|
||||
"loading": {
|
||||
"message": "Loading...",
|
||||
"description": "Message shown on the loading screen before we've loaded any messages"
|
||||
},
|
||||
"exportInstructions": {
|
||||
"message": "The first step is to choose a directory to store this application's exported data. It will contain your message history and sensitive cryptographic data, so be sure to save it somewhere private.",
|
||||
"description": "Description of the export process"
|
||||
},
|
||||
"chooseDirectory": {
|
||||
"message": "Choose directory",
|
||||
"description": "Button to allow the user to export all data from app as part of migration process"
|
||||
"message": "Choose folder",
|
||||
"description": "Button to allow the user to find a folder on disk"
|
||||
},
|
||||
"exportButton": {
|
||||
"message": "Export",
|
||||
"desription": "Button shown on the choose directory dialog which starts the export process"
|
||||
"loadDataHeader": {
|
||||
"message": "Load your data",
|
||||
"description": "Header shown on the first screen in the data import process"
|
||||
},
|
||||
"exportChooserTitle": {
|
||||
"message": "Choose target directory for data",
|
||||
"description": "Title of the popup window used to select data storage location"
|
||||
},
|
||||
"exportAgain": {
|
||||
"message": "Export again",
|
||||
"description": "If user has already exported once, this button allows user to do it again if needed"
|
||||
},
|
||||
"exportError": {
|
||||
"message": "Unfortunately, something went wrong during the export. First, double-check your target empty directory for write access and enough space. Then, please submit a debug log so we can help you get migrated!",
|
||||
"description": "Helper text if the user went forward on migrating the app, but ran into an error"
|
||||
},
|
||||
"exporting": {
|
||||
"message": "Please wait while we export your data. It may take several minutes. You can still use Signal on your phone and other devices during this time.",
|
||||
"description": "Message shown on the migration screen while we export data"
|
||||
},
|
||||
"exportComplete": {
|
||||
"message": "Your data has been exported to: <p><b>$location$</b></p> You'll be able to import this data as you set up <a target='_blank' href='https://support.whispersystems.org/hc/en-us/articles/214507138'>the new Signal Desktop</a>.",
|
||||
"description": "Message shown on the migration screen when we are done exporting data",
|
||||
"placeholders": {
|
||||
"location": {
|
||||
"content": "$1",
|
||||
"example": "/Users/someone/somewhere"
|
||||
}
|
||||
}
|
||||
},
|
||||
"installNewSignal": {
|
||||
"message": "Install new Signal Desktop",
|
||||
"description": "When export is complete, a button shows which sends user to Signal Desktop install instructions"
|
||||
},
|
||||
"importButton": {
|
||||
"message": "Import",
|
||||
"desription": "Button shown on the choose directory dialog which starts the import process"
|
||||
"loadDataDescription": {
|
||||
"message": "You've just gone through the export process, and your contacts and messages are waiting patiently on your computer. Select the folder that contains your saved Signal data.",
|
||||
"description": "Introduction to the process of importing messages and contacts from disk"
|
||||
},
|
||||
"importChooserTitle": {
|
||||
"message": "Choose directory with exported data",
|
||||
"description": "Title of the popup window used to select data previously exported"
|
||||
},
|
||||
"importErrorHeader": {
|
||||
"message": "Something went wrong!",
|
||||
"description": "Header of the error screen after a failed import"
|
||||
},
|
||||
"importingHeader": {
|
||||
"message": "Loading contacts and messages",
|
||||
"description": "Header of screen shown as data is import"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Unfortunately, something went wrong during the import. <p>First, double-check your target directory. It should start with 'Signal Export.'</p><p>Next, try with a new export of your data from the Chrome App.</p>If that still fails, please <a target='_blank' href='https://support.whispersystems.org/hc/en-us/articles/215188737'>submit a debug log</a> so we can help you get migrated!",
|
||||
"message": "Make sure you have chosen the correct directory that contains your saved Signal data. Its name should begin with 'Signal Export.' You can also save a new copy of your data from the Chrome App.<p>If these steps don't work for you, please <a target='_blank' href='https://support.signal.org/hc/en-us/articles/215188737'>submit a debug log</a> so that we can help you get migrated!</p>",
|
||||
"description": "Message shown if the import went wrong."
|
||||
},
|
||||
"tryAgain": {
|
||||
"message": "Try again",
|
||||
"importAgain": {
|
||||
"message": "Choose folder and try again",
|
||||
"description": "Button shown if the user runs into an error during import, allowing them to start over"
|
||||
},
|
||||
"importInstructions": {
|
||||
"message": "The first step is to tell us where you previously <a href='https://support.signal.org/hc/en-us/articles/115002502511'>exported your Signal data</a>. It will be a directory whose name starts with 'Signal Export.'<br><br><b>NOTE</b>: You must only import a set of exported data <b>once</b>. Import makes a copy of the exported client, and duplicate clients interfere with each other.",
|
||||
"description": "Description of the export process"
|
||||
"importCompleteHeader": {
|
||||
"message": "Success!",
|
||||
"description": "Header shown on the screen at the end of a successful import process"
|
||||
},
|
||||
"importing": {
|
||||
"message": "Please wait while we import your data...",
|
||||
"description": "Shown as we are loading the user's data from disk"
|
||||
"importCompleteStartButton": {
|
||||
"message": "Start using Signal Desktop",
|
||||
"description": "Button shown at end of successful import process, nothing left but a restart"
|
||||
},
|
||||
"importComplete": {
|
||||
"message": "We've successfully loaded your data. The next step is to restart the application!",
|
||||
"description": "Shown when the import is complete."
|
||||
"importCompleteLinkButton": {
|
||||
"message": "Link this device to your phone",
|
||||
"description": "Button shown at end of successful 'light' import process, so the standard linking process still needs to happen"
|
||||
},
|
||||
"selectedLocation": {
|
||||
"message": "your selected location",
|
||||
|
@ -526,87 +512,46 @@
|
|||
"message": "Privacy is possible. Signal makes it easy.",
|
||||
"description": "Tagline displayed under 'installWelcome' string on the install page"
|
||||
},
|
||||
"installNew": {
|
||||
"message": "Set up as new install",
|
||||
"description": "One of two choices presented on the screen shown on first launch"
|
||||
"linkYourPhone": {
|
||||
"message": "Link your phone to Signal Desktop",
|
||||
"description": "Shown on the front page when the application first starst, above the QR code"
|
||||
},
|
||||
"installImport": {
|
||||
"message": "Set up with exported data",
|
||||
"description": "One of two choices presented on the screen shown on first launch"
|
||||
"signalSettings": {
|
||||
"message": "Signal Settings",
|
||||
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
|
||||
},
|
||||
"installGetStartedButton": {
|
||||
"message": "Get started"
|
||||
"linkedDevices": {
|
||||
"message": "Linked Devices",
|
||||
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
|
||||
},
|
||||
"installSignalLink": {
|
||||
"message": "First, install <a $a_params$>Signal</a> on your mobile phone. We'll link your devices and keep your messages in sync.",
|
||||
"description": "Prompt the user to install Signal on their phone before linking",
|
||||
"placeholders": {
|
||||
"a_params": {
|
||||
"content": "$1",
|
||||
"example": "href='http://example.com'"
|
||||
}
|
||||
}
|
||||
"plusButton": {
|
||||
"message": "'+' Button",
|
||||
"description": "The button used in Signal Android to add a new linked device"
|
||||
},
|
||||
"installSignalLinks": {
|
||||
"message": "First, install Signal on your <a $play_store$>Android</a> or <a $app_store$>iPhone</a>.<br /> We'll link your devices and keep your messages in sync.",
|
||||
"description": "Prompt the user to install Signal on their phone before linking",
|
||||
"placeholders": {
|
||||
"play_store": {
|
||||
"content": "$1",
|
||||
"example": "href='http://example.com'"
|
||||
},
|
||||
"app_store": {
|
||||
"content": "$2",
|
||||
"example": "href='http://example.com'"
|
||||
}
|
||||
}
|
||||
"linkNewDevice": {
|
||||
"message": "Link New Device",
|
||||
"description": "The menu option shown in Signal iOS to add a new linked device"
|
||||
},
|
||||
"installGotIt": {
|
||||
"message": "Got it",
|
||||
"description": "Button for the user to confirm that they have Signal installed."
|
||||
"deviceName": {
|
||||
"message": "Device name",
|
||||
"description": "The label in settings panel shown for the user-provided name for this desktop instance"
|
||||
},
|
||||
"installIHaveSignalButton": {
|
||||
"message": "I have Signal for Android",
|
||||
"description": "Button for the user to confirm that they have Signal for Android"
|
||||
"chooseDeviceName": {
|
||||
"message": "Choose this device's name",
|
||||
"description": "The header shown on the 'choose device name' screen in the device linking process"
|
||||
},
|
||||
"installFollowUs": {
|
||||
"message": "<a $a_params$>Follow us</a> for updates about multi-device support for iOS.",
|
||||
"placeholders": {
|
||||
"a_params": {
|
||||
"content": "$1",
|
||||
"example": "href='http://example.com'"
|
||||
}
|
||||
}
|
||||
"finishLinkingPhone": {
|
||||
"message": "Finish linking phone",
|
||||
"description": "The text on the button to finish the linking process, after choosing the device name"
|
||||
},
|
||||
"installAndroidInstructions": {
|
||||
"message": "Open Signal on your phone and navigate to Settings > Linked devices. Tap the button to add a new device, then scan the code above."
|
||||
},
|
||||
"installConnecting": {
|
||||
"message": "Connecting...",
|
||||
"description": "Displayed when waiting for the QR Code"
|
||||
"initialSync": {
|
||||
"message": "Syncing contacts and groups",
|
||||
"description": "Shown during initial link while contacts and groups are being pulled from mobile device"
|
||||
},
|
||||
"installConnectionFailed": {
|
||||
"message": "Failed to connect to server.",
|
||||
"description": "Displayed when we can't connect to the server."
|
||||
},
|
||||
"installGeneratingKeys": {
|
||||
"message": "Generating Keys"
|
||||
},
|
||||
"installSyncingGroupsAndContacts": {
|
||||
"message": "Syncing groups and contacts"
|
||||
},
|
||||
"installComputerName": {
|
||||
"message": "This computer's name will be",
|
||||
"description": "Text displayed before the input where the user can enter the name for this device."
|
||||
},
|
||||
"installLinkingWithNumber": {
|
||||
"message": "Linking with",
|
||||
"description": "Text displayed before the phone number that the user is in the process of linking with"
|
||||
},
|
||||
"installFinalButton": {
|
||||
"message": "Looking good",
|
||||
"description": "The final button for the install process, after the user has entered a name for their device"
|
||||
},
|
||||
"installTooManyDevices": {
|
||||
"message": "Sorry, you have too many devices linked already. Try removing some."
|
||||
},
|
||||
|
@ -748,6 +693,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"timerSetOnSync": {
|
||||
"message": "Updating timer to $time$.",
|
||||
"description": "Message displayed when timer is set on initial link of desktop device.",
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"content": "$1",
|
||||
"example": "10m"
|
||||
}
|
||||
}
|
||||
},
|
||||
"theyChangedTheTimer": {
|
||||
"message": "$name$ set the timer to $time$.",
|
||||
"description": "Message displayed when someone else changes the message expiration timer in a conversation.",
|
||||
|
@ -908,10 +863,10 @@
|
|||
"message": "There is a new version of Signal available."
|
||||
},
|
||||
"autoUpdateNewVersionInstructions": {
|
||||
"message": "Press Restart to apply the updates."
|
||||
"message": "Press Restart Signal to apply the updates."
|
||||
},
|
||||
"autoUpdateRestartButtonLabel": {
|
||||
"message": "Restart"
|
||||
"message": "Restart Signal"
|
||||
},
|
||||
"autoUpdateLaterButtonLabel": {
|
||||
"message": "Later"
|
||||
|
|
|
@ -42,7 +42,7 @@ function showUpdateDialog(mainWindow, messages) {
|
|||
if (response === RESTART_BUTTON) {
|
||||
// We delay these update calls because they don't seem to work in this
|
||||
// callback - but only if the message box has a parent window.
|
||||
// Fixes this bug: https://github.com/WhisperSystems/Signal-Desktop/issues/1864
|
||||
// Fixes this bug: https://github.com/signalapp/Signal-Desktop/issues/1864
|
||||
setTimeout(() => {
|
||||
windowState.markShouldQuit();
|
||||
autoUpdater.quitAndInstall();
|
||||
|
|
|
@ -2,8 +2,14 @@ const path = require('path');
|
|||
|
||||
const electronIsDev = require('electron-is-dev');
|
||||
|
||||
const defaultEnvironment = electronIsDev ? 'development' : 'production';
|
||||
const environment = process.env.NODE_ENV || defaultEnvironment;
|
||||
let environment;
|
||||
|
||||
// In production mode, NODE_ENV cannot be customized by the user
|
||||
if (electronIsDev) {
|
||||
environment = process.env.NODE_ENV || 'development';
|
||||
} else {
|
||||
environment = 'production';
|
||||
}
|
||||
|
||||
// Set environment vars to configure node-config before requiring it
|
||||
process.env.NODE_ENV = environment;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
|
@ -117,8 +120,8 @@ function eliminateOutOfDateFiles(logPath, date) {
|
|||
const file = {
|
||||
path: target,
|
||||
start: isLineAfterDate(start, date),
|
||||
end: isLineAfterDate(end[end.length - 1], date)
|
||||
|| isLineAfterDate(end[end.length - 2], date),
|
||||
end: isLineAfterDate(end[end.length - 1], date) ||
|
||||
isLineAfterDate(end[end.length - 2], date),
|
||||
};
|
||||
|
||||
if (!file.start && !file.end) {
|
||||
|
|
61
app/menu.js
|
@ -6,6 +6,9 @@ function createTemplate(options, messages) {
|
|||
openNewBugForm,
|
||||
openSupportPage,
|
||||
openForums,
|
||||
setupWithImport,
|
||||
setupAsNewDevice,
|
||||
setupAsStandalone,
|
||||
} = options;
|
||||
|
||||
const template = [{
|
||||
|
@ -123,6 +126,27 @@ function createTemplate(options, messages) {
|
|||
],
|
||||
}];
|
||||
|
||||
if (options.includeSetup) {
|
||||
const fileMenu = template[0];
|
||||
|
||||
// These are in reverse order, since we're prepending them one at a time
|
||||
if (options.development) {
|
||||
fileMenu.submenu.unshift({
|
||||
label: messages.menuSetupAsStandalone.message,
|
||||
click: setupAsStandalone,
|
||||
});
|
||||
}
|
||||
|
||||
fileMenu.submenu.unshift({
|
||||
label: messages.menuSetupAsNewDevice.message,
|
||||
click: setupAsNewDevice,
|
||||
});
|
||||
fileMenu.submenu.unshift({
|
||||
label: messages.menuSetupWithImport.message,
|
||||
click: setupWithImport,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
return updateForMac(template, messages, options);
|
||||
}
|
||||
|
@ -134,14 +158,46 @@ function updateForMac(template, messages, options) {
|
|||
const {
|
||||
showWindow,
|
||||
showAbout,
|
||||
setupWithImport,
|
||||
setupAsNewDevice,
|
||||
setupAsStandalone,
|
||||
} = options;
|
||||
|
||||
// Remove About item and separator from Help menu, since it's on the first menu
|
||||
template[4].submenu.pop();
|
||||
template[4].submenu.pop();
|
||||
|
||||
// Replace File menu
|
||||
// Remove File menu
|
||||
template.shift();
|
||||
|
||||
if (options.includeSetup) {
|
||||
// Add a File menu just for these setup options. Because we're using unshift(), we add
|
||||
// the file menu first, though it ends up to the right of the Signal Desktop menu.
|
||||
const fileMenu = {
|
||||
label: messages.mainMenuFile.message,
|
||||
submenu: [
|
||||
{
|
||||
label: messages.menuSetupWithImport.message,
|
||||
click: setupWithImport,
|
||||
},
|
||||
{
|
||||
label: messages.menuSetupAsNewDevice.message,
|
||||
click: setupAsNewDevice,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (options.development) {
|
||||
fileMenu.submenu.push({
|
||||
label: messages.menuSetupAsStandalone.message,
|
||||
click: setupAsStandalone,
|
||||
});
|
||||
}
|
||||
|
||||
template.unshift(fileMenu);
|
||||
}
|
||||
|
||||
// Add the OSX-specific Signal Desktop menu at the far left
|
||||
template.unshift({
|
||||
submenu: [
|
||||
{
|
||||
|
@ -170,7 +226,8 @@ function updateForMac(template, messages, options) {
|
|||
});
|
||||
|
||||
// Add to Edit menu
|
||||
template[1].submenu.push(
|
||||
const editIndex = options.includeSetup ? 2 : 1;
|
||||
template[editIndex].submenu.push(
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
|
|
|
@ -19,7 +19,7 @@ build_script:
|
|||
- node build\grunt.js
|
||||
- type package.json | findstr /v certificateSubjectName > temp.json
|
||||
- move temp.json package.json
|
||||
- yarn prepare-build
|
||||
- yarn prepare-beta-build
|
||||
- node_modules\.bin\build --em.environment=%SIGNAL_ENV% --publish=never
|
||||
|
||||
test_script:
|
||||
|
|
245
background.html
|
@ -8,7 +8,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none';
|
||||
connect-src 'self' wss: https:;
|
||||
connect-src 'self' https: wss:;
|
||||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' blob: data:;
|
||||
|
@ -536,7 +536,7 @@
|
|||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='link_to_support'>
|
||||
<a href='http://support.whispersystems.org/hc/articles/213134107' target='_blank'>
|
||||
<a href='http://support.signal.org/hc/articles/213134107' target='_blank'>
|
||||
{{ learnMore }}
|
||||
</a>
|
||||
</script>
|
||||
|
@ -562,7 +562,7 @@
|
|||
</div>
|
||||
<p>
|
||||
<a class='report-link' target='_blank'
|
||||
href='https://github.com/WhisperSystems/Signal-Desktop/issues/new/'>
|
||||
href='https://github.com/signalapp/Signal-Desktop/issues/new/'>
|
||||
{{ reportIssue }}
|
||||
</a>
|
||||
</p>
|
||||
|
@ -571,6 +571,9 @@
|
|||
<div class='content'>
|
||||
<a class='x close' alt='close settings' href='#'></a>
|
||||
<h2>{{ settings }}</h2>
|
||||
<div class='device-name-settings'>
|
||||
<b>{{ deviceNameLabel }}:</b> {{ deviceName }}
|
||||
</div>
|
||||
<hr>
|
||||
<div class='theme-settings'>
|
||||
<h3>{{ theme }}</h3>
|
||||
|
@ -652,151 +655,198 @@
|
|||
{{/action }}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='install-choice'>
|
||||
<div class='step'>
|
||||
<script type='text/x-tmpl-mustache' id='import-flow-template'>
|
||||
{{#isStep2}}
|
||||
<div id='step2' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<img id='signal-icon' src='images/icon_250.png'/>
|
||||
<h1>{{ installWelcome }}</h1>
|
||||
<p>{{ installTagline }}</p>
|
||||
<span class='banner-icon folder-outline'></span>
|
||||
<div class='header'>{{ chooseHeader }}</div>
|
||||
<div class='body-text'>{{ choose }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div> <a class='button new'>{{ installNew }}</a> </div>
|
||||
<div> <a class='button import'>{{ installImport }}</a> </div>
|
||||
<div>
|
||||
<a class='button choose'>{{ chooseButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep2}}
|
||||
{{#isStep3}}
|
||||
<div id='step3' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon import'></span>
|
||||
<div class='header'>{{ importingHeader }}</div>
|
||||
</div>
|
||||
<div class='progress'>
|
||||
<div class='bar-container'>
|
||||
<div class='bar progress-bar progress-bar-striped active'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep3}}
|
||||
{{#isStep4}}
|
||||
<div id='step4' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon check-circle-outline'></span>
|
||||
<div class='header'>{{ completeHeader }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
{{#restartButton}}
|
||||
<div>
|
||||
<a class='button restart'>{{ restartButton }}</a>
|
||||
</div>
|
||||
{{/restartButton}}
|
||||
{{#registerButton}}
|
||||
<div>
|
||||
<a class='button register'>{{ registerButton }}</a>
|
||||
</div>
|
||||
{{/registerButton}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep4}}
|
||||
|
||||
{{#isError}}
|
||||
<div id='error' class='step'>
|
||||
<div class='inner error-dialog clearfix'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline'></span>
|
||||
<div class='header'>{{ errorHeader }}</div>
|
||||
<div class='body-text-wide'>{{& errorMessage }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div>
|
||||
<a class='button choose'>{{ chooseButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isError}}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='install_flow_template'>
|
||||
|
||||
<div id='step2' class='step hidden'>
|
||||
<script type='text/x-tmpl-mustache' id='link-flow-template'>
|
||||
{{#isStep3}}
|
||||
<div id='step3' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<img id='signal-phone' src='images/signal-phone.png'>
|
||||
<p>{{{ installSignalLink }}}</p>
|
||||
<div class='header'>{{ linkYourPhone }}</div>
|
||||
<div id="qr">
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div> <a class='button step3'>{{ installIHaveSignalButton }}</a> </div>
|
||||
<div class='dot-container'>
|
||||
<span class='dot step1'></span>
|
||||
<span class='dot step2 selected'></span>
|
||||
<span class='dot step3'></span>
|
||||
<div class='instructions'>
|
||||
<div class='android'>
|
||||
<div class='label'>
|
||||
<span class='os-icon android'></span>
|
||||
</div>
|
||||
<div class='body'>
|
||||
<div>→ {{ signalSettings }}</div>
|
||||
<div>→ {{ linkedDevices }}</div>
|
||||
<div>→ {{ androidFinalStep }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='apple'>
|
||||
<div class='label'>
|
||||
<span class='os-icon apple'></span>
|
||||
</div>
|
||||
<div class='body'>
|
||||
<div>→ {{ signalSettings }}</div>
|
||||
<div>→ {{ linkedDevices }}</div>
|
||||
<div>→ {{ appleFinalStep }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='step3' class='step hidden'>
|
||||
{{/isStep3}}
|
||||
{{#isStep4}}
|
||||
<div id='step4' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<div id="qr"></div>
|
||||
<p>{{ installAndroidInstructions }}</p>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
{{ #development }}
|
||||
<div> <a class='button openStandalone'>Standalone</a> </div>
|
||||
{{ /development }}
|
||||
<div class='dot-container'>
|
||||
<span class='dot step1'></span>
|
||||
<span class='dot step2'></span>
|
||||
<span class='dot step3 selected'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id='step4' class='step hidden'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<p>{{ installLinkingWithNumber }}</p>
|
||||
<h2 class='number'></h2>
|
||||
<img id='signal-computer' src='images/signal-laptop.png'>
|
||||
<p>{{ installComputerName }}</p>
|
||||
<span class='banner-icon lead-pencil'></span>
|
||||
<div class='header'>{{ chooseName }}</div>
|
||||
<div>
|
||||
<input type='text' id='device-name' spellcheck='false' maxlength='50' />
|
||||
<input type='text' class='device-name' spellcheck='false' maxlength='50' />
|
||||
</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div>
|
||||
<input type='submit' class='button' id='sync' value='{{ installFinalButton }}' />
|
||||
<a class='button finish'>{{ finishLinkingPhoneButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id='step5' class='step hidden'>
|
||||
</div>
|
||||
{{/isStep4}}
|
||||
{{#isStep5}}
|
||||
<div id='step5' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<img id='signal-icon' src='images/icon_250.png'/>
|
||||
<div class='progress-dialog'>
|
||||
<p class='status'></p>
|
||||
<div class='bar-container'><div class='bar progress-bar'></div></div>
|
||||
<span class='banner-icon sync'></span>
|
||||
<div class='header'>{{ syncing }}</div>
|
||||
</div>
|
||||
<div class='progress'>
|
||||
<div class='bar-container'>
|
||||
<div class='bar progress-bar progress-bar-striped active'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep5}}
|
||||
|
||||
<div id='stepTooManyDevices' class='step hidden'>
|
||||
<div class='inner error-dialog clearfix'>
|
||||
<div class='panel step-body'>{{ installTooManyDevices }}</div>
|
||||
<div class='nav'>
|
||||
<button class='ok step3'>{{ ok }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='stepNetworkError' class='step hidden'>
|
||||
<div class='inner error-dialog clearfix'>
|
||||
<div class='panel step-body'>{{ installConnectionFailed }}</div>
|
||||
<button class='ok step3 button'>{{ tryAgain }}</button>
|
||||
{{#isError}}
|
||||
<div id='error' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline'></span>
|
||||
<div class='header'>{{ errorHeader }}</div>
|
||||
<div class='body'>{{ errorMessage }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<a class='button try-again'>{{ errorButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isError}}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='standalone'>
|
||||
<header>
|
||||
<div class='container'>
|
||||
<div class='row'>
|
||||
<div class='col-xs-2 col-md-1'>
|
||||
<img id='textsecure-icon' src='images/icon_250.png'/>
|
||||
</div>
|
||||
<div class='col-xs-10 col-md-11'>
|
||||
<h1>Create your Signal Account</h1>
|
||||
<h4 class='tagline'>Private messaging from your web browser.</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class='container'>
|
||||
<div class='col-xs-offset-1 col-md-6'>
|
||||
<div class='narrow'>
|
||||
<div id="phone-number-input">
|
||||
<div class="phone-input-form">
|
||||
<div id="number-container" class="number-container">
|
||||
<input type="tel" class="number" placeholder="Phone Number" />
|
||||
<div class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<img class='banner-image' src='images/icon_128.png' />
|
||||
<div class='header'>Create your Signal Account</div>
|
||||
<div id='phone-number-input'>
|
||||
<div class='phone-input-form'>
|
||||
<div id='number-container' class='number-container'>
|
||||
<input type='tel' class='number' placeholder='Phone Number' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='clearfix'>
|
||||
<button id="request-sms" class="btn btn-info">Send SMS</button>
|
||||
<button id="request-voice" class="btn btn-info" tabindex=-1>Call</button>
|
||||
<a class='button' id='request-sms'>Send SMS</a>
|
||||
<a class='link' id='request-voice' tabindex=-1>Call</a>
|
||||
</div>
|
||||
<form id='form'>
|
||||
<h2></h2>
|
||||
<input class='form-control' type="text" pattern="\s*[0-9]{3}-?[0-9]{3}\s*" title="Enter your 6-digit verification code. If you did not receive a code, click Call or Send SMS to request a new one" id="code" placeholder="Verification Code" autocomplete='off'>
|
||||
<button id="verifyCode" class="btn btn-info" data-loading-text="Please wait...">Register</button>
|
||||
<div id='error' class='collapse'></div>
|
||||
<div> <a class='button openInstaller'>Link to phone</a> </div>
|
||||
</form>
|
||||
<input class='form-control' type='text' pattern='\s*[0-9]{3}-?[0-9]{3}\s*' title='Enter your 6-digit verification code. If you did not receive a code, click Call or Send SMS to request a new one' id='code' placeholder='Verification Code' autocomplete='off'>
|
||||
<div id='error' class='collapse'></div>
|
||||
<div id=status></div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<a class='button' id='verifyCode' data-loading-text='Please wait...'>Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/javascript' src='js/components.js'></script>
|
||||
<script type='text/javascript' src='js/reliable_trigger.js'></script>
|
||||
<script type='text/javascript' src='js/database.js'></script>
|
||||
|
@ -857,7 +907,6 @@
|
|||
<script type="text/javascript" src="js/views/phone-input-view.js"></script>
|
||||
<script type='text/javascript' src='js/views/standalone_registration_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/app_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/install_choice_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/import_view.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
|
||||
|
|
13
bower.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "textsecure-chrome",
|
||||
"name": "signal-desktop",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/WhisperSystems/TextSecure-Browser",
|
||||
"homepage": "https://github.com/signalapp/Signal-Desktop",
|
||||
"license": "GPLV3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
@ -15,7 +15,6 @@
|
|||
"indexeddb-backbonejs-adapter": "*",
|
||||
"intl-tel-input": "~4.0.1",
|
||||
"blueimp-load-image": "~1.13.0",
|
||||
"blueimp-canvas-to-blob": "~2.1.1",
|
||||
"autosize": "~4.0.0",
|
||||
"webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git",
|
||||
"mp3lameencoder": "https://github.com/higuma/mp3-lame-encoder-js.git",
|
||||
|
@ -69,12 +68,6 @@
|
|||
"build/img/flags.png",
|
||||
"build/js/intlTelInput.js"
|
||||
],
|
||||
"blueimp-load-image": [
|
||||
"js/load-image.js"
|
||||
],
|
||||
"blueimp-canvas-to-blob": [
|
||||
"js/canvas-to-blob.js"
|
||||
],
|
||||
"emojijs": [
|
||||
"lib/emoji.js",
|
||||
"demo/emoji.css"
|
||||
|
@ -113,8 +106,6 @@
|
|||
"moment",
|
||||
"intl-tel-input",
|
||||
"backbone.typeahead",
|
||||
"blueimp-load-image",
|
||||
"blueimp-canvas-to-blob",
|
||||
"autosize",
|
||||
"filesize"
|
||||
],
|
||||
|
|
|
@ -4,5 +4,8 @@
|
|||
"disableAutoUpdate": false,
|
||||
"openDevTools": false,
|
||||
"buildExpiration": 0,
|
||||
"certificateAuthorities": ["-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n"]
|
||||
"certificateAuthorities": [
|
||||
"-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n"
|
||||
],
|
||||
"import": false
|
||||
}
|
||||
|
|
1
images/alert-outline.svg
Normal file
|
@ -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
|
@ -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
|
@ -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 |
1
images/check-circle-outline.svg
Normal 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 |
1
images/folder-outline.svg
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": false
|
||||
},
|
||||
}
|
328
js/background.js
|
@ -1,13 +1,35 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
/* eslint-disable */
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
/* global Backbone: false */
|
||||
/* global $: false */
|
||||
|
||||
/* global ConversationController: false */
|
||||
/* global getAccountManager: false */
|
||||
/* global Signal: false */
|
||||
/* global storage: false */
|
||||
/* global textsecure: false */
|
||||
/* global Whisper: false */
|
||||
/* global wrapDeferred: false */
|
||||
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
const { Message } = window.Signal.Types;
|
||||
|
||||
// Implicitly used in `indexeddb-backbonejs-adapter`:
|
||||
// https://github.com/signalapp/Signal-Desktop/blob/4033a9f8137e62ed286170ed5d4941982b1d3a64/components/indexeddb-backbonejs-adapter/backbone-indexeddb.js#L569
|
||||
window.onInvalidStateError = function(e) {
|
||||
console.log(e);
|
||||
};
|
||||
|
||||
window.wrapDeferred = function(deferred) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
deferred.then(resolve, reject);
|
||||
});
|
||||
};
|
||||
|
||||
console.log('background page reloaded');
|
||||
console.log('environment:', window.config.environment);
|
||||
|
||||
|
@ -85,6 +107,27 @@
|
|||
}
|
||||
});
|
||||
|
||||
Whisper.events.on('setupWithImport', function() {
|
||||
var appView = window.owsDesktopApp.appView;
|
||||
if (appView) {
|
||||
appView.openImporter();
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.events.on('setupAsNewDevice', function() {
|
||||
var appView = window.owsDesktopApp.appView;
|
||||
if (appView) {
|
||||
appView.openInstaller();
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.events.on('setupAsStandalone', function() {
|
||||
var appView = window.owsDesktopApp.appView;
|
||||
if (appView) {
|
||||
appView.openStandalone();
|
||||
}
|
||||
});
|
||||
|
||||
function start() {
|
||||
var currentVersion = window.config.version;
|
||||
var lastVersion = storage.get('version');
|
||||
|
@ -118,8 +161,10 @@
|
|||
appView.openInbox({
|
||||
initialLoadComplete: initialLoadComplete
|
||||
});
|
||||
} else if (window.config.importMode) {
|
||||
appView.openImporter();
|
||||
} else {
|
||||
appView.openInstallChoice();
|
||||
appView.openInstaller();
|
||||
}
|
||||
|
||||
Whisper.events.on('showDebugLog', function() {
|
||||
|
@ -136,12 +181,6 @@
|
|||
appView.openInbox();
|
||||
}
|
||||
});
|
||||
Whisper.events.on('contactsync:begin', function() {
|
||||
if (appView.installView && appView.installView.showSync) {
|
||||
appView.installView.showSync();
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.Notifications.on('click', function(conversation) {
|
||||
showWindow();
|
||||
if (conversation) {
|
||||
|
@ -266,7 +305,7 @@
|
|||
messageReceiver.addEventListener('error', onError);
|
||||
messageReceiver.addEventListener('empty', onEmpty);
|
||||
messageReceiver.addEventListener('progress', onProgress);
|
||||
messageReceiver.addEventListener('settings', onSettings);
|
||||
messageReceiver.addEventListener('configuration', onConfiguration);
|
||||
|
||||
window.textsecure.messaging = new textsecure.MessageSender(
|
||||
SERVER_URL, USERNAME, PASSWORD, CDN_URL
|
||||
|
@ -345,12 +384,8 @@
|
|||
view.onProgress(count);
|
||||
}
|
||||
}
|
||||
function onSettings(ev) {
|
||||
if (ev.settings.readReceipts) {
|
||||
storage.put('read-receipt-setting', true);
|
||||
} else {
|
||||
storage.put('read-receipt-setting', false);
|
||||
}
|
||||
function onConfiguration(ev) {
|
||||
storage.put('read-receipt-setting', ev.configuration.readReceipts);
|
||||
}
|
||||
|
||||
function onContactReceived(ev) {
|
||||
|
@ -377,38 +412,58 @@
|
|||
|
||||
return ConversationController.getOrCreateAndWait(id, 'private')
|
||||
.then(function(conversation) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var activeAt = conversation.get('active_at');
|
||||
var activeAt = conversation.get('active_at');
|
||||
|
||||
// The idea is to make any new contact show up in the left pane. If
|
||||
// activeAt is null, then this contact has been purposefully hidden.
|
||||
if (activeAt !== null) {
|
||||
activeAt = activeAt || Date.now();
|
||||
}
|
||||
// The idea is to make any new contact show up in the left pane. If
|
||||
// activeAt is null, then this contact has been purposefully hidden.
|
||||
if (activeAt !== null) {
|
||||
activeAt = activeAt || Date.now();
|
||||
}
|
||||
|
||||
if (details.profileKey) {
|
||||
conversation.set({profileKey: details.profileKey});
|
||||
if (details.profileKey) {
|
||||
conversation.set({profileKey: details.profileKey});
|
||||
}
|
||||
|
||||
if (typeof details.blocked !== 'undefined') {
|
||||
if (details.blocked) {
|
||||
storage.addBlockedNumber(id);
|
||||
} else {
|
||||
storage.removeBlockedNumber(id);
|
||||
}
|
||||
conversation.save({
|
||||
name: details.name,
|
||||
avatar: details.avatar,
|
||||
color: details.color,
|
||||
active_at: activeAt,
|
||||
}).then(resolve, reject);
|
||||
}).then(function() {
|
||||
if (details.verified) {
|
||||
var verified = details.verified;
|
||||
var ev = new Event('verified');
|
||||
ev.verified = {
|
||||
state: verified.state,
|
||||
destination: verified.destination,
|
||||
identityKey: verified.identityKey.toArrayBuffer(),
|
||||
};
|
||||
ev.viaContactSync = true;
|
||||
return onVerified(ev);
|
||||
}
|
||||
|
||||
return wrapDeferred(conversation.save({
|
||||
name: details.name,
|
||||
avatar: details.avatar,
|
||||
color: details.color,
|
||||
active_at: activeAt,
|
||||
})).then(function() {
|
||||
// this needs to be inline to get access to conversation model
|
||||
if (typeof details.expireTimer !== 'undefined') {
|
||||
var source = textsecure.storage.user.getNumber();
|
||||
var receivedAt = Date.now();
|
||||
return conversation.updateExpirationTimer(
|
||||
details.expireTimer,
|
||||
source,
|
||||
receivedAt,
|
||||
{fromSync: true}
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
if (details.verified) {
|
||||
var verified = details.verified;
|
||||
var ev = new Event('verified');
|
||||
ev.verified = {
|
||||
state: verified.state,
|
||||
destination: verified.destination,
|
||||
identityKey: verified.identityKey.toArrayBuffer(),
|
||||
};
|
||||
ev.viaContactSync = true;
|
||||
return onVerified(ev);
|
||||
}
|
||||
})
|
||||
.then(ev.confirm)
|
||||
.catch(function(error) {
|
||||
console.log(
|
||||
|
@ -437,93 +492,138 @@
|
|||
if (activeAt !== null) {
|
||||
updates.active_at = activeAt || Date.now();
|
||||
}
|
||||
updates.left = false;
|
||||
} else {
|
||||
updates.left = true;
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
conversation.save(updates).then(resolve, reject);
|
||||
|
||||
return wrapDeferred(conversation.save(updates)).then(function() {
|
||||
if (typeof details.expireTimer !== 'undefined') {
|
||||
var source = textsecure.storage.user.getNumber();
|
||||
var receivedAt = Date.now();
|
||||
return conversation.updateExpirationTimer(
|
||||
details.expireTimer,
|
||||
source,
|
||||
receivedAt,
|
||||
{fromSync: true}
|
||||
);
|
||||
}
|
||||
}).then(ev.confirm);
|
||||
});
|
||||
}
|
||||
|
||||
function onMessageReceived(ev) {
|
||||
var data = ev.data;
|
||||
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
||||
var profileKey = data.message.profileKey.toArrayBuffer();
|
||||
return ConversationController.getOrCreateAndWait(data.source, 'private').then(function(sender) {
|
||||
return sender.setProfileKey(profileKey).then(ev.confirm);
|
||||
});
|
||||
}
|
||||
var message = initIncomingMessage(data);
|
||||
/* eslint-enable */
|
||||
/* jshint ignore:start */
|
||||
|
||||
return isMessageDuplicate(message).then(function(isDuplicate) {
|
||||
if (isDuplicate) {
|
||||
console.log('Received duplicate message', message.idForLogging());
|
||||
ev.confirm();
|
||||
return;
|
||||
}
|
||||
// Descriptors
|
||||
const getGroupDescriptor = group => ({
|
||||
type: Message.GROUP,
|
||||
id: group.id,
|
||||
});
|
||||
|
||||
var type, id;
|
||||
if (data.message.group) {
|
||||
type = 'group';
|
||||
id = data.message.group.id;
|
||||
} else {
|
||||
type = 'private';
|
||||
id = data.source;
|
||||
}
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
||||
const getDescriptorForSent = ({ message, destination }) => (
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: { type: Message.PRIVATE, id: destination }
|
||||
);
|
||||
|
||||
return ConversationController.getOrCreateAndWait(id, type).then(function() {
|
||||
return message.handleDataMessage(data.message, ev.confirm, {
|
||||
initialLoadComplete: initialLoadComplete
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||
const getDescriptorForReceived = ({ message, source }) => (
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: { type: Message.PRIVATE, id: source }
|
||||
);
|
||||
|
||||
function onSentMessage(ev) {
|
||||
var now = new Date().getTime();
|
||||
var data = ev.data;
|
||||
function createMessageHandler({
|
||||
createMessage,
|
||||
getMessageDescriptor,
|
||||
handleProfileUpdate,
|
||||
}) {
|
||||
return async (event) => {
|
||||
const { data, confirm } = event;
|
||||
|
||||
var type, id;
|
||||
if (data.message.group) {
|
||||
type = 'group';
|
||||
id = data.message.group.id;
|
||||
} else {
|
||||
type = 'private';
|
||||
id = data.destination;
|
||||
}
|
||||
const messageDescriptor = getMessageDescriptor(data);
|
||||
|
||||
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
||||
return ConversationController.getOrCreateAndWait(id, type).then(function(convo) {
|
||||
return convo.save({profileSharing: true}).then(ev.confirm);
|
||||
});
|
||||
}
|
||||
const { PROFILE_KEY_UPDATE } = textsecure.protobuf.DataMessage.Flags;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
|
||||
if (isProfileUpdate) {
|
||||
return handleProfileUpdate({ data, confirm, messageDescriptor });
|
||||
}
|
||||
|
||||
var message = new Whisper.Message({
|
||||
source : textsecure.storage.user.getNumber(),
|
||||
sourceDevice : data.device,
|
||||
sent_at : data.timestamp,
|
||||
received_at : now,
|
||||
conversationId : data.destination,
|
||||
type : 'outgoing',
|
||||
sent : true,
|
||||
expirationStartTimestamp: data.expirationStartTimestamp,
|
||||
});
|
||||
const message = createMessage(data);
|
||||
const isDuplicate = await isMessageDuplicate(message);
|
||||
if (isDuplicate) {
|
||||
console.log('Received duplicate message', message.idForLogging());
|
||||
return event.confirm();
|
||||
}
|
||||
|
||||
return isMessageDuplicate(message).then(function(isDuplicate) {
|
||||
if (isDuplicate) {
|
||||
console.log('Received duplicate message', message.idForLogging());
|
||||
ev.confirm();
|
||||
return;
|
||||
}
|
||||
const upgradedMessage = await Message.upgradeSchema(data.message);
|
||||
await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
);
|
||||
return message.handleDataMessage(
|
||||
upgradedMessage,
|
||||
event.confirm,
|
||||
{ initialLoadComplete }
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return ConversationController.getOrCreateAndWait(id, type).then(function() {
|
||||
return message.handleDataMessage(data.message, ev.confirm, {
|
||||
initialLoadComplete: initialLoadComplete
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// Received:
|
||||
async function handleMessageReceivedProfileUpdate({
|
||||
data,
|
||||
confirm,
|
||||
messageDescriptor,
|
||||
}) {
|
||||
const profileKey = data.message.profileKey.toArrayBuffer();
|
||||
const sender = await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
'private'
|
||||
);
|
||||
await sender.setProfileKey(profileKey);
|
||||
return confirm();
|
||||
}
|
||||
|
||||
const onMessageReceived = createMessageHandler({
|
||||
handleProfileUpdate: handleMessageReceivedProfileUpdate,
|
||||
getMessageDescriptor: getDescriptorForReceived,
|
||||
createMessage: initIncomingMessage,
|
||||
});
|
||||
|
||||
// Sent:
|
||||
async function handleMessageSentProfileUpdate({ confirm, messageDescriptor }) {
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
);
|
||||
await conversation.save({ profileSharing: true });
|
||||
return confirm();
|
||||
}
|
||||
|
||||
function createSentMessage(data) {
|
||||
const now = Date.now();
|
||||
return new Whisper.Message({
|
||||
source: textsecure.storage.user.getNumber(),
|
||||
sourceDevice: data.device,
|
||||
sent_at: data.timestamp,
|
||||
received_at: now,
|
||||
conversationId: data.destination,
|
||||
type: 'outgoing',
|
||||
sent: true,
|
||||
expirationStartTimestamp: data.expirationStartTimestamp,
|
||||
});
|
||||
}
|
||||
|
||||
const onSentMessage = createMessageHandler({
|
||||
handleProfileUpdate: handleMessageSentProfileUpdate,
|
||||
getMessageDescriptor: getDescriptorForSent,
|
||||
createMessage: createSentMessage,
|
||||
});
|
||||
/* jshint ignore:end */
|
||||
/* eslint-disable */
|
||||
|
||||
function isMessageDuplicate(message) {
|
||||
return new Promise(function(resolve) {
|
||||
|
|
283
js/backup.js
|
@ -75,9 +75,9 @@
|
|||
};
|
||||
}
|
||||
|
||||
function exportNonMessages(idb_db, parent) {
|
||||
function exportNonMessages(idb_db, parent, options) {
|
||||
return createFileAndWriter(parent, 'db.json').then(function(writer) {
|
||||
return exportToJsonFile(idb_db, writer);
|
||||
return exportToJsonFile(idb_db, writer, options);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -85,10 +85,27 @@
|
|||
* Export all data from an IndexedDB database
|
||||
* @param {IDBDatabase} idb_db
|
||||
*/
|
||||
function exportToJsonFile(idb_db, fileWriter) {
|
||||
function exportToJsonFile(idb_db, fileWriter, options) {
|
||||
options = options || {};
|
||||
_.defaults(options, {excludeClientConfig: false});
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var storeNames = idb_db.objectStoreNames;
|
||||
storeNames = _.without(storeNames, 'messages');
|
||||
|
||||
if (options.excludeClientConfig) {
|
||||
console.log('exportToJsonFile: excluding client config from export');
|
||||
storeNames = _.without(
|
||||
storeNames,
|
||||
'items',
|
||||
'signedPreKeys',
|
||||
'preKeys',
|
||||
'identityKeys',
|
||||
'sessions',
|
||||
'unprocessed' // since we won't be able to decrypt them anyway
|
||||
);
|
||||
}
|
||||
|
||||
var exportedStoreNames = [];
|
||||
if (storeNames.length === 0) {
|
||||
throw new Error('No stores to export');
|
||||
|
@ -160,9 +177,10 @@
|
|||
});
|
||||
}
|
||||
|
||||
function importNonMessages(idb_db, parent) {
|
||||
return readFileAsText(parent, 'db.json').then(function(string) {
|
||||
return importFromJsonString(idb_db, string);
|
||||
function importNonMessages(idb_db, parent, options) {
|
||||
var file = 'db.json';
|
||||
return readFileAsText(parent, file).then(function(string) {
|
||||
return importFromJsonString(idb_db, string, path.join(parent, file), options);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -176,6 +194,16 @@
|
|||
reject(error || new Error(prefix));
|
||||
}
|
||||
|
||||
function eliminateClientConfigInBackup(data, path) {
|
||||
var cleaned = _.pick(data, 'conversations', 'groups');
|
||||
console.log('Writing configuration-free backup file back to disk');
|
||||
try {
|
||||
fs.writeFileSync(path, JSON.stringify(cleaned));
|
||||
} catch (error) {
|
||||
console.log('Error writing cleaned-up backup to disk: ', error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import data from JSON into an IndexedDB database. This does not delete any existing data
|
||||
* from the database, so keys could clash
|
||||
|
@ -183,19 +211,50 @@
|
|||
* @param {IDBDatabase} idb_db
|
||||
* @param {string} jsonString - data to import, one key per object store
|
||||
*/
|
||||
function importFromJsonString(idb_db, jsonString) {
|
||||
function importFromJsonString(idb_db, jsonString, path, options) {
|
||||
options = options || {};
|
||||
_.defaults(options, {
|
||||
forceLightImport: false,
|
||||
conversationLookup: {},
|
||||
groupLookup: {},
|
||||
});
|
||||
|
||||
var conversationLookup = options.conversationLookup;
|
||||
var groupLookup = options.groupLookup;
|
||||
var result = {
|
||||
fullImport: true,
|
||||
};
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var importObject = JSON.parse(jsonString);
|
||||
delete importObject.debug;
|
||||
var storeNames = _.keys(importObject);
|
||||
|
||||
if (!importObject.sessions || options.forceLightImport) {
|
||||
result.fullImport = false;
|
||||
|
||||
delete importObject.items;
|
||||
delete importObject.signedPreKeys;
|
||||
delete importObject.preKeys;
|
||||
delete importObject.identityKeys;
|
||||
delete importObject.sessions;
|
||||
delete importObject.unprocessed;
|
||||
|
||||
console.log('This is a light import; contacts, groups and messages only');
|
||||
}
|
||||
|
||||
// We mutate the on-disk backup to prevent the user from importing client
|
||||
// configuration more than once - that causes lots of encryption errors.
|
||||
// This of course preserves the true data: conversations and groups.
|
||||
eliminateClientConfigInBackup(importObject, path);
|
||||
|
||||
var storeNames = _.keys(importObject);
|
||||
console.log('Importing to these stores:', storeNames.join(', '));
|
||||
|
||||
var finished = false;
|
||||
var finish = function(via) {
|
||||
console.log('non-messages import done via', via);
|
||||
if (finished) {
|
||||
resolve();
|
||||
resolve(result);
|
||||
}
|
||||
finished = true;
|
||||
};
|
||||
|
@ -219,20 +278,46 @@
|
|||
}
|
||||
|
||||
var count = 0;
|
||||
var skipCount = 0;
|
||||
|
||||
var finishStore = function() {
|
||||
// added all objects for this store
|
||||
delete importObject[storeName];
|
||||
console.log(
|
||||
'Done importing to store',
|
||||
storeName,
|
||||
'Total count:',
|
||||
count,
|
||||
'Skipped:',
|
||||
skipCount
|
||||
);
|
||||
if (_.keys(importObject).length === 0) {
|
||||
// added all object stores
|
||||
console.log('DB import complete');
|
||||
finish('puts scheduled');
|
||||
}
|
||||
};
|
||||
|
||||
_.each(importObject[storeName], function(toAdd) {
|
||||
toAdd = unstringify(toAdd);
|
||||
|
||||
var haveConversationAlready =
|
||||
storeName === 'conversations'
|
||||
&& conversationLookup[getConversationKey(toAdd)];
|
||||
var haveGroupAlready =
|
||||
storeName === 'groups' && groupLookup[getGroupKey(toAdd)];
|
||||
|
||||
if (haveConversationAlready || haveGroupAlready) {
|
||||
skipCount++;
|
||||
count++;
|
||||
return;
|
||||
}
|
||||
|
||||
var request = transaction.objectStore(storeName).put(toAdd, toAdd.id);
|
||||
request.onsuccess = function(event) {
|
||||
count++;
|
||||
if (count == importObject[storeName].length) {
|
||||
// added all objects for this store
|
||||
delete importObject[storeName];
|
||||
console.log('Done importing to store', storeName);
|
||||
if (_.keys(importObject).length === 0) {
|
||||
// added all object stores
|
||||
console.log('DB import complete');
|
||||
finish('puts scheduled');
|
||||
}
|
||||
finishStore();
|
||||
}
|
||||
};
|
||||
request.onerror = function(e) {
|
||||
|
@ -243,6 +328,12 @@
|
|||
);
|
||||
};
|
||||
});
|
||||
|
||||
// We have to check here, because we may have skipped every item, resulting
|
||||
// in no onsuccess callback at all.
|
||||
if (count === importObject[storeName].length) {
|
||||
finishStore();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -432,14 +523,20 @@
|
|||
request.onsuccess = function(event) {
|
||||
var cursor = event.target.result;
|
||||
if (cursor) {
|
||||
if (count !== 0) {
|
||||
stream.write(',');
|
||||
}
|
||||
|
||||
var message = cursor.value;
|
||||
var messageId = message.received_at;
|
||||
var attachments = message.attachments;
|
||||
|
||||
// skip message if it is disappearing, no matter the amount of time left
|
||||
if (message.expireTimer) {
|
||||
cursor.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (count !== 0) {
|
||||
stream.write(',');
|
||||
}
|
||||
|
||||
message.attachments = _.map(attachments, function(attachment) {
|
||||
return _.omit(attachment, ['data']);
|
||||
});
|
||||
|
@ -598,6 +695,10 @@
|
|||
}));
|
||||
}
|
||||
|
||||
function saveMessage(idb_db, message) {
|
||||
return saveAllMessages(idb_db, [message]);
|
||||
}
|
||||
|
||||
function saveAllMessages(idb_db, messages) {
|
||||
if (!messages.length) {
|
||||
return Promise.resolve();
|
||||
|
@ -658,43 +759,64 @@
|
|||
// message, save it, and only then do we move on to the next message. Thus, every
|
||||
// message with attachments needs to be removed from our overall message save with the
|
||||
// filter() call.
|
||||
function importConversation(idb_db, dir) {
|
||||
function importConversation(idb_db, dir, options) {
|
||||
options = options || {};
|
||||
_.defaults(options, {messageLookup: {}});
|
||||
|
||||
var messageLookup = options.messageLookup;
|
||||
var conversationId = 'unknown';
|
||||
var total = 0;
|
||||
var skipped = 0;
|
||||
|
||||
return readFileAsText(dir, 'messages.json').then(function(contents) {
|
||||
var promiseChain = Promise.resolve();
|
||||
|
||||
var json = JSON.parse(contents);
|
||||
var conversationId;
|
||||
if (json.messages && json.messages.length) {
|
||||
conversationId = json.messages[0].conversationId;
|
||||
conversationId = '[REDACTED]' + (json.messages[0].conversationId || '').slice(-3);
|
||||
}
|
||||
total = json.messages.length;
|
||||
|
||||
var messages = _.filter(json.messages, function(message) {
|
||||
message = unstringify(message);
|
||||
|
||||
if (messageLookup[getMessageKey(message)]) {
|
||||
skipped++;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.attachments && message.attachments.length) {
|
||||
var process = function() {
|
||||
return loadAttachments(dir, message).then(function() {
|
||||
return saveAllMessages(idb_db, [message]);
|
||||
return saveMessage(idb_db, message);
|
||||
});
|
||||
};
|
||||
|
||||
promiseChain = promiseChain.then(process);
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return message;
|
||||
return true;
|
||||
});
|
||||
|
||||
return saveAllMessages(idb_db, messages)
|
||||
var promise = Promise.resolve();
|
||||
if (messages.length > 0) {
|
||||
promise = saveAllMessages(idb_db, messages);
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(function() {
|
||||
return promiseChain;
|
||||
})
|
||||
.then(function() {
|
||||
console.log(
|
||||
'Finished importing conversation',
|
||||
// Don't know if group or private conversation, so we blindly redact
|
||||
conversationId ? '[REDACTED]' + conversationId.slice(-3) : 'with no messages'
|
||||
conversationId,
|
||||
'Total:',
|
||||
total,
|
||||
'Skipped:',
|
||||
skipped
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -703,7 +825,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
function importConversations(idb_db, dir) {
|
||||
function importConversations(idb_db, dir, options) {
|
||||
return getDirContents(dir).then(function(contents) {
|
||||
var promiseChain = Promise.resolve();
|
||||
|
||||
|
@ -713,7 +835,7 @@
|
|||
}
|
||||
|
||||
var process = function() {
|
||||
return importConversation(idb_db, conversationDir);
|
||||
return importConversation(idb_db, conversationDir, options);
|
||||
};
|
||||
|
||||
promiseChain = promiseChain.then(process);
|
||||
|
@ -723,6 +845,73 @@
|
|||
});
|
||||
}
|
||||
|
||||
function getMessageKey(message) {
|
||||
var ourNumber = textsecure.storage.user.getNumber();
|
||||
var source = message.source || ourNumber;
|
||||
if (source === ourNumber) {
|
||||
return source + ' ' + message.timestamp;
|
||||
}
|
||||
|
||||
var sourceDevice = message.sourceDevice || 1;
|
||||
return source + '.' + sourceDevice + ' ' + message.timestamp;
|
||||
}
|
||||
function loadMessagesLookup(idb_db) {
|
||||
return assembleLookup(idb_db, 'messages', getMessageKey);
|
||||
}
|
||||
|
||||
function getConversationKey(conversation) {
|
||||
return conversation.id;
|
||||
}
|
||||
function loadConversationLookup(idb_db) {
|
||||
return assembleLookup(idb_db, 'conversations', getConversationKey);
|
||||
}
|
||||
|
||||
function getGroupKey(group) {
|
||||
return group.id;
|
||||
}
|
||||
function loadGroupsLookup(idb_db) {
|
||||
return assembleLookup(idb_db, 'groups', getGroupKey);
|
||||
}
|
||||
|
||||
function assembleLookup(idb_db, storeName, keyFunction) {
|
||||
var lookup = Object.create(null);
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var transaction = idb_db.transaction(storeName, 'readwrite');
|
||||
transaction.onerror = function(e) {
|
||||
handleDOMException(
|
||||
'assembleLookup(' + storeName + ') transaction error',
|
||||
transaction.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
transaction.oncomplete = function() {
|
||||
// not really very useful - fires at unexpected times
|
||||
};
|
||||
|
||||
var promiseChain = Promise.resolve();
|
||||
var store = transaction.objectStore(storeName);
|
||||
var request = store.openCursor();
|
||||
request.onerror = function(e) {
|
||||
handleDOMException(
|
||||
'assembleLookup(' + storeName + ') request error',
|
||||
request.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = function(event) {
|
||||
var cursor = event.target.result;
|
||||
if (cursor && cursor.value) {
|
||||
lookup[keyFunction(cursor.value)] = true;
|
||||
cursor.continue();
|
||||
} else {
|
||||
console.log('Done creating ' + storeName + ' lookup');
|
||||
return resolve(lookup);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function clearAllStores(idb_db) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
console.log('Clearing all indexeddb stores');
|
||||
|
@ -791,7 +980,7 @@
|
|||
};
|
||||
return getDirectory(options);
|
||||
},
|
||||
backupToDirectory: function(directory) {
|
||||
exportToDirectory: function(directory, options) {
|
||||
var dir;
|
||||
var idb;
|
||||
return openDatabase().then(function(idb_db) {
|
||||
|
@ -800,7 +989,7 @@
|
|||
return createDirectory(directory, name);
|
||||
}).then(function(created) {
|
||||
dir = created;
|
||||
return exportNonMessages(idb, dir);
|
||||
return exportNonMessages(idb, dir, options);
|
||||
}).then(function() {
|
||||
return exportConversations(idb, dir);
|
||||
}).then(function() {
|
||||
|
@ -823,18 +1012,30 @@
|
|||
};
|
||||
return getDirectory(options);
|
||||
},
|
||||
importFromDirectory: function(directory) {
|
||||
var idb;
|
||||
importFromDirectory: function(directory, options) {
|
||||
options = options || {};
|
||||
|
||||
var idb, nonMessageResult;
|
||||
return openDatabase().then(function(idb_db) {
|
||||
idb = idb_db;
|
||||
return importNonMessages(idb_db, directory);
|
||||
|
||||
return Promise.all([
|
||||
loadMessagesLookup(idb_db),
|
||||
loadConversationLookup(idb_db),
|
||||
loadGroupsLookup(idb_db),
|
||||
]);
|
||||
}).then(function(lookups) {
|
||||
options.messageLookup = lookups[0];
|
||||
options.conversationLookup = lookups[1];
|
||||
options.groupLookup = lookups[2];
|
||||
}).then(function() {
|
||||
return importConversations(idb, directory);
|
||||
return importNonMessages(idb, directory, options);
|
||||
}).then(function(result) {
|
||||
nonMessageResult = result;
|
||||
return importConversations(idb, directory, options);
|
||||
}).then(function() {
|
||||
return directory;
|
||||
}).then(function(path) {
|
||||
console.log('done restoring from backup!');
|
||||
return path;
|
||||
return nonMessageResult;
|
||||
}, function(error) {
|
||||
console.log(
|
||||
'the import went wrong:',
|
||||
|
|
|
@ -3,8 +3,27 @@
|
|||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
storage.isBlocked = function(number) {
|
||||
return storage.get('blocked', []).indexOf(number) >= 0;
|
||||
var numbers = storage.get('blocked', []);
|
||||
|
||||
return _.include(numbers, number);
|
||||
};
|
||||
storage.addBlockedNumber = function(number) {
|
||||
var numbers = storage.get('blocked', []);
|
||||
if (_.include(numbers, number)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('adding', number, 'to blocked list');
|
||||
storage.put('blocked', numbers.concat(number));
|
||||
};
|
||||
storage.removeBlockedNumber = function(number) {
|
||||
var numbers = storage.get('blocked', []);
|
||||
if (!_.include(numbers, number)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('removing', number, 'from blocked list');
|
||||
storage.put('blocked', _.without(numbers, number));
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
/* eslint-disable */
|
||||
|
||||
/* global Signal: false */
|
||||
/* global storage: false */
|
||||
/* global textsecure: false */
|
||||
/* global Whisper: false */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { Attachment, Message } = window.Signal.Types;
|
||||
|
||||
// TODO: Factor out private and group subclasses of Conversation
|
||||
|
||||
var COLORS = [
|
||||
|
@ -32,13 +38,13 @@
|
|||
if (ab1.byteLength !== ab2.byteLength) {
|
||||
return false;
|
||||
}
|
||||
var result = true;
|
||||
var result = 0;
|
||||
var ta1 = new Uint8Array(ab1);
|
||||
var ta2 = new Uint8Array(ab2);
|
||||
for (var i = 0; i < ab1.byteLength; ++i) {
|
||||
if (ta1[i] !== ta2[i]) { result = false; }
|
||||
result = result | ta1[i] ^ ta2[i];
|
||||
}
|
||||
return result;
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
Whisper.Conversation = Backbone.Model.extend({
|
||||
|
@ -598,54 +604,71 @@
|
|||
}
|
||||
},
|
||||
|
||||
sendMessage: function(body, attachments) {
|
||||
this.queueJob(function() {
|
||||
var now = Date.now();
|
||||
/* jshint ignore:start */
|
||||
/* eslint-enable */
|
||||
sendMessage(body, attachments) {
|
||||
this.queueJob(async () => {
|
||||
const now = Date.now();
|
||||
|
||||
console.log(
|
||||
'Sending message to conversation',
|
||||
this.idForLogging(),
|
||||
'with timestamp',
|
||||
now
|
||||
);
|
||||
console.log(
|
||||
'Sending message to conversation',
|
||||
this.idForLogging(),
|
||||
'with timestamp',
|
||||
now
|
||||
);
|
||||
|
||||
var message = this.messageCollection.add({
|
||||
body : body,
|
||||
conversationId : this.id,
|
||||
type : 'outgoing',
|
||||
attachments : attachments,
|
||||
sent_at : now,
|
||||
received_at : now,
|
||||
expireTimer : this.get('expireTimer'),
|
||||
recipients : this.getRecipients()
|
||||
});
|
||||
if (this.isPrivate()) {
|
||||
message.set({destination: this.id});
|
||||
}
|
||||
message.save();
|
||||
const upgradedAttachments =
|
||||
await Promise.all(attachments.map(Attachment.upgradeSchema));
|
||||
const message = this.messageCollection.add({
|
||||
body,
|
||||
conversationId: this.id,
|
||||
type: 'outgoing',
|
||||
attachments: upgradedAttachments,
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
expireTimer: this.get('expireTimer'),
|
||||
recipients: this.getRecipients(),
|
||||
});
|
||||
if (this.isPrivate()) {
|
||||
message.set({ destination: this.id });
|
||||
}
|
||||
message.save();
|
||||
|
||||
this.save({
|
||||
active_at : now,
|
||||
timestamp : now,
|
||||
lastMessage : message.getNotificationText()
|
||||
});
|
||||
this.save({
|
||||
active_at: now,
|
||||
timestamp: now,
|
||||
lastMessage: message.getNotificationText(),
|
||||
});
|
||||
|
||||
var sendFunc;
|
||||
if (this.get('type') == 'private') {
|
||||
sendFunc = textsecure.messaging.sendMessageToNumber;
|
||||
}
|
||||
else {
|
||||
sendFunc = textsecure.messaging.sendMessageToGroup;
|
||||
}
|
||||
const conversationType = this.get('type');
|
||||
const sendFunc = (() => {
|
||||
switch (conversationType) {
|
||||
case Message.PRIVATE:
|
||||
return textsecure.messaging.sendMessageToNumber;
|
||||
case Message.GROUP:
|
||||
return textsecure.messaging.sendMessageToGroup;
|
||||
default:
|
||||
throw new TypeError(`Invalid conversation type: '${conversationType}'`);
|
||||
}
|
||||
})();
|
||||
|
||||
var profileKey;
|
||||
if (this.get('profileSharing')) {
|
||||
profileKey = storage.get('profileKey');
|
||||
}
|
||||
let profileKey;
|
||||
if (this.get('profileSharing')) {
|
||||
profileKey = storage.get('profileKey');
|
||||
}
|
||||
|
||||
message.send(sendFunc(this.get('id'), body, attachments, now, this.get('expireTimer'), profileKey));
|
||||
}.bind(this));
|
||||
message.send(sendFunc(
|
||||
this.get('id'),
|
||||
body,
|
||||
upgradedAttachments,
|
||||
now,
|
||||
this.get('expireTimer'),
|
||||
profileKey
|
||||
));
|
||||
});
|
||||
},
|
||||
/* jshint ignore:end */
|
||||
/* eslint-disable */
|
||||
|
||||
updateLastMessage: function() {
|
||||
var collection = new Whisper.MessageCollection();
|
||||
|
@ -668,11 +691,28 @@
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
updateExpirationTimer: function(expireTimer, source, received_at) {
|
||||
if (!expireTimer) { expireTimer = null; }
|
||||
updateExpirationTimer: function(expireTimer, source, received_at, options) {
|
||||
options = options || {};
|
||||
_.defaults(options, {fromSync: false});
|
||||
|
||||
if (!expireTimer) {
|
||||
expireTimer = null;
|
||||
}
|
||||
if (this.get('expireTimer') === expireTimer
|
||||
|| (!expireTimer && !this.get('expireTimer'))) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Updating expireTimer for conversation',
|
||||
this.idForLogging(),
|
||||
'via',
|
||||
source
|
||||
);
|
||||
source = source || textsecure.storage.user.getNumber();
|
||||
var timestamp = received_at || Date.now();
|
||||
this.save({ expireTimer: expireTimer });
|
||||
|
||||
var message = this.messageCollection.add({
|
||||
conversationId : this.id,
|
||||
type : received_at ? 'incoming' : 'outgoing',
|
||||
|
@ -681,7 +721,8 @@
|
|||
flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
expirationTimerUpdate : {
|
||||
expireTimer : expireTimer,
|
||||
source : source
|
||||
source : source,
|
||||
fromSync : options.fromSync,
|
||||
}
|
||||
});
|
||||
if (this.isPrivate()) {
|
||||
|
@ -690,8 +731,16 @@
|
|||
if (message.isOutgoing()) {
|
||||
message.set({recipients: this.getRecipients() });
|
||||
}
|
||||
message.save();
|
||||
if (message.isOutgoing()) { // outgoing update, send it to the number/group
|
||||
|
||||
return Promise.all([
|
||||
wrapDeferred(message.save()),
|
||||
wrapDeferred(this.save({ expireTimer: expireTimer })),
|
||||
]).then(function() {
|
||||
if (message.isIncoming()) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// change was made locally, send it to the number/group
|
||||
var sendFunc;
|
||||
if (this.get('type') == 'private') {
|
||||
sendFunc = textsecure.messaging.sendExpirationTimerUpdateToNumber;
|
||||
|
@ -703,9 +752,16 @@
|
|||
if (this.get('profileSharing')) {
|
||||
profileKey = storage.get('profileKey');
|
||||
}
|
||||
message.send(sendFunc(this.get('id'), this.get('expireTimer'), message.get('sent_at'), profileKey));
|
||||
}
|
||||
return message;
|
||||
var promise = sendFunc(this.get('id'),
|
||||
this.get('expireTimer'),
|
||||
message.get('sent_at'),
|
||||
profileKey
|
||||
);
|
||||
|
||||
return message.send(promise).then(function() {
|
||||
return message;
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
isSearchable: function() {
|
||||
|
@ -855,6 +911,11 @@
|
|||
},
|
||||
|
||||
getProfile: function(id) {
|
||||
if (!textsecure.messaging) {
|
||||
var message = 'Conversation.getProfile: textsecure.messaging not available';
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
|
||||
return textsecure.messaging.getProfile(id).then(function(profile) {
|
||||
var identityKey = dcodeIO.ByteBuffer.wrap(profile.identityKey, 'base64').toArrayBuffer();
|
||||
|
||||
|
|
|
@ -373,7 +373,7 @@
|
|||
// 1. on an incoming message
|
||||
// 2. on a sent message sync'd from another device
|
||||
// 3. in rare cases, an incoming message can be retried, though it will
|
||||
// still through one of the previous two codepaths.
|
||||
// still go through one of the previous two codepaths
|
||||
var message = this;
|
||||
var source = message.get('source');
|
||||
var type = message.get('type');
|
||||
|
|
10
js/modules/.eslintrc
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": false,
|
||||
"commonjs": true,
|
||||
"node": false
|
||||
},
|
||||
"globals": {
|
||||
"console": true
|
||||
}
|
||||
}
|
40
js/modules/auto_orient_image.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
const loadImage = require('blueimp-load-image');
|
||||
|
||||
const DEFAULT_JPEG_QUALITY = 0.85;
|
||||
|
||||
// File | Blob | URLString -> LoadImageOptions -> Promise<DataURLString>
|
||||
//
|
||||
// Documentation for `options` (`LoadImageOptions`):
|
||||
// https://github.com/blueimp/JavaScript-Load-Image/tree/v2.18.0#options
|
||||
exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
|
||||
const optionsWithDefaults = Object.assign(
|
||||
{
|
||||
type: 'image/jpeg',
|
||||
quality: DEFAULT_JPEG_QUALITY,
|
||||
},
|
||||
options,
|
||||
{
|
||||
canvas: true,
|
||||
orientation: true,
|
||||
}
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
loadImage(fileOrBlobOrURL, (canvasOrError) => {
|
||||
if (canvasOrError.type === 'error') {
|
||||
const error = new Error('autoOrientImage: Failed to process image');
|
||||
error.cause = canvasOrError;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasOrError;
|
||||
const dataURL = canvas.toDataURL(
|
||||
optionsWithDefaults.type,
|
||||
optionsWithDefaults.quality
|
||||
);
|
||||
|
||||
resolve(dataURL);
|
||||
}, optionsWithDefaults);
|
||||
});
|
||||
};
|
185
js/modules/types/attachment.js
Normal 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, we’d preserve the original image data for users who want to
|
||||
// retain it but due to reports of data loss, we don’t want to overburden IndexedDB
|
||||
// by potentially doubling stored image data.
|
||||
// See: https://github.com/signalapp/Signal-Desktop/issues/1589
|
||||
const newAttachment = Object.assign({}, attachment, {
|
||||
data: newDataArrayBuffer,
|
||||
size: newDataArrayBuffer.byteLength,
|
||||
});
|
||||
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
delete newAttachment.digest;
|
||||
|
||||
return newAttachment;
|
||||
};
|
||||
|
||||
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 doesn’t 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));
|
17
js/modules/types/message.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const Attachment = require('./attachment');
|
||||
|
||||
|
||||
const GROUP = 'group';
|
||||
const PRIVATE = 'private';
|
||||
|
||||
// Public API
|
||||
exports.GROUP = GROUP;
|
||||
exports.PRIVATE = PRIVATE;
|
||||
|
||||
// Schema
|
||||
// Message -> Promise Message
|
||||
exports.upgradeSchema = async message =>
|
||||
Object.assign({}, message, {
|
||||
attachments:
|
||||
await Promise.all(message.attachments.map(Attachment.upgradeSchema)),
|
||||
});
|
2
js/modules/types/mime.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
exports.isJPEG = mimeType =>
|
||||
mimeType === 'image/jpeg';
|
|
@ -78,13 +78,13 @@
|
|||
if (ab1.byteLength !== ab2.byteLength) {
|
||||
return false;
|
||||
}
|
||||
var result = true;
|
||||
var result = 0;
|
||||
var ta1 = new Uint8Array(ab1);
|
||||
var ta2 = new Uint8Array(ab2);
|
||||
for (var i = 0; i < ab1.byteLength; ++i) {
|
||||
if (ta1[i] !== ta2[i]) { result = false; }
|
||||
result = result | ta1[i] ^ ta2[i];
|
||||
}
|
||||
return result;
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
var Model = Backbone.Model.extend({ database: Whisper.Database });
|
||||
|
@ -688,7 +688,7 @@
|
|||
}.bind(this));
|
||||
},
|
||||
// This matches the Java method as of
|
||||
// https://github.com/WhisperSystems/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
|
||||
// https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
|
||||
processVerifiedMessage: function(identifier, verifiedStatus, publicKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error("Tried to set verified for undefined/null key");
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
initialize: function(options) {
|
||||
this.inboxView = null;
|
||||
this.installView = null;
|
||||
|
||||
this.applyTheme();
|
||||
this.applyHideMenu();
|
||||
},
|
||||
events: {
|
||||
'click .openInstaller': 'openInstaller',
|
||||
'click .openStandalone': 'openStandalone',
|
||||
'click .openInstaller': 'openInstaller', // NetworkStatusView has this button
|
||||
'openInbox': 'openInbox',
|
||||
'change-theme': 'applyTheme',
|
||||
'change-hide-menu': 'applyHideMenu',
|
||||
|
@ -45,39 +45,38 @@
|
|||
this.debugLogView = null;
|
||||
}
|
||||
},
|
||||
openInstallChoice: function() {
|
||||
this.closeInstallChoice();
|
||||
var installChoice = this.installChoice = new Whisper.InstallChoiceView();
|
||||
|
||||
this.listenTo(installChoice, 'install-new', this.openInstaller.bind(this));
|
||||
this.listenTo(installChoice, 'install-import', this.openImporter.bind(this));
|
||||
|
||||
this.openView(this.installChoice);
|
||||
},
|
||||
closeInstallChoice: function() {
|
||||
if (this.installChoice) {
|
||||
this.installChoice.remove();
|
||||
this.installChoice = null;
|
||||
}
|
||||
},
|
||||
openImporter: function() {
|
||||
this.closeImporter();
|
||||
this.closeInstallChoice();
|
||||
window.addSetupMenuItems();
|
||||
this.resetViews();
|
||||
var importView = this.importView = new Whisper.ImportView();
|
||||
this.listenTo(importView, 'cancel', this.openInstallChoice.bind(this));
|
||||
this.listenTo(importView, 'light-import', this.finishLightImport.bind(this));
|
||||
this.openView(this.importView);
|
||||
},
|
||||
finishLightImport: function() {
|
||||
var options = {
|
||||
hasExistingData: true
|
||||
};
|
||||
this.openInstaller(options);
|
||||
},
|
||||
closeImporter: function() {
|
||||
if (this.importView) {
|
||||
this.importView.remove();
|
||||
this.importView = null;
|
||||
}
|
||||
},
|
||||
openInstaller: function() {
|
||||
this.closeInstaller();
|
||||
this.closeInstallChoice();
|
||||
var installView = this.installView = new Whisper.InstallView();
|
||||
this.listenTo(installView, 'cancel', this.openInstallChoice.bind(this));
|
||||
openInstaller: function(options) {
|
||||
options = options || {};
|
||||
|
||||
// If we're in the middle of import, we don't want to show the menu options
|
||||
// allowing the user to switch to other ways to set up the app. If they
|
||||
// switched back and forth in the middle of a light import, they'd lose all
|
||||
// that imported data.
|
||||
if (!options.hasExistingData) {
|
||||
window.addSetupMenuItems();
|
||||
}
|
||||
|
||||
this.resetViews();
|
||||
var installView = this.installView = new Whisper.InstallView(options);
|
||||
this.openView(this.installView);
|
||||
},
|
||||
closeInstaller: function() {
|
||||
|
@ -88,11 +87,23 @@
|
|||
},
|
||||
openStandalone: function() {
|
||||
if (window.config.environment !== 'production') {
|
||||
this.closeInstaller();
|
||||
this.installView = new Whisper.StandaloneRegistrationView();
|
||||
this.openView(this.installView);
|
||||
window.addSetupMenuItems();
|
||||
this.resetViews();
|
||||
this.standaloneView = new Whisper.StandaloneRegistrationView();
|
||||
this.openView(this.standaloneView);
|
||||
}
|
||||
},
|
||||
closeStandalone: function() {
|
||||
if (this.standaloneView) {
|
||||
this.standaloneView.remove();
|
||||
this.standaloneView = null;
|
||||
}
|
||||
},
|
||||
resetViews: function() {
|
||||
this.closeInstaller();
|
||||
this.closeImporter();
|
||||
this.closeStandalone();
|
||||
},
|
||||
openInbox: function(options) {
|
||||
options = options || {};
|
||||
// The inbox can be created before the 'empty' event fires or afterwards. If
|
||||
|
|
|
@ -1,271 +1,290 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
/* eslint-env browser */
|
||||
|
||||
/* global $: false */
|
||||
/* global _: false */
|
||||
/* global Backbone: false */
|
||||
/* global moment: false */
|
||||
|
||||
/* global i18n: false */
|
||||
/* global textsecure: false */
|
||||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
'use strict';
|
||||
const ESCAPE_KEY_CODE = 27;
|
||||
|
||||
var FileView = Whisper.View.extend({
|
||||
tagName: 'div',
|
||||
className: 'fileView',
|
||||
templateName: 'file-view',
|
||||
render_attributes: function() {
|
||||
return this.model;
|
||||
}
|
||||
const FileView = Whisper.View.extend({
|
||||
tagName: 'div',
|
||||
className: 'fileView',
|
||||
templateName: 'file-view',
|
||||
render_attributes() {
|
||||
return this.model;
|
||||
},
|
||||
});
|
||||
|
||||
var ImageView = Backbone.View.extend({
|
||||
tagName: 'img',
|
||||
initialize: function(dataUrl) {
|
||||
this.dataUrl = dataUrl;
|
||||
},
|
||||
events: {
|
||||
'load': 'update',
|
||||
},
|
||||
update: function() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render: function() {
|
||||
this.$el.attr('src', this.dataUrl);
|
||||
return this;
|
||||
}
|
||||
const ImageView = Backbone.View.extend({
|
||||
tagName: 'img',
|
||||
initialize(blobUrl) {
|
||||
this.blobUrl = blobUrl;
|
||||
},
|
||||
events: {
|
||||
load: 'update',
|
||||
},
|
||||
update() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render() {
|
||||
this.$el.attr('src', this.blobUrl);
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
var MediaView = Backbone.View.extend({
|
||||
initialize: function(dataUrl, contentType) {
|
||||
this.dataUrl = dataUrl;
|
||||
this.contentType = contentType;
|
||||
this.$el.attr('controls', '');
|
||||
},
|
||||
events: {
|
||||
'canplay': 'canplay'
|
||||
},
|
||||
canplay: function() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render: function() {
|
||||
var $el = $('<source>');
|
||||
$el.attr('src', this.dataUrl);
|
||||
this.$el.append($el);
|
||||
return this;
|
||||
}
|
||||
const MediaView = Backbone.View.extend({
|
||||
initialize(dataUrl, { contentType } = {}) {
|
||||
this.dataUrl = dataUrl;
|
||||
this.contentType = contentType;
|
||||
this.$el.attr('controls', '');
|
||||
},
|
||||
events: {
|
||||
canplay: 'canplay',
|
||||
},
|
||||
canplay() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render() {
|
||||
const $el = $('<source>');
|
||||
$el.attr('src', this.dataUrl);
|
||||
this.$el.append($el);
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
var AudioView = MediaView.extend({ tagName: 'audio' });
|
||||
var VideoView = MediaView.extend({ tagName: 'video' });
|
||||
const AudioView = MediaView.extend({ tagName: 'audio' });
|
||||
const VideoView = MediaView.extend({ tagName: 'video' });
|
||||
|
||||
// Blacklist common file types known to be unsupported in Chrome
|
||||
var UnsupportedFileTypes = [
|
||||
const UnsupportedFileTypes = [
|
||||
'audio/aiff',
|
||||
'video/quicktime'
|
||||
'video/quicktime',
|
||||
];
|
||||
|
||||
Whisper.AttachmentView = Backbone.View.extend({
|
||||
tagName: 'span',
|
||||
className: function() {
|
||||
className() {
|
||||
if (this.isImage()) {
|
||||
return 'attachment';
|
||||
} else {
|
||||
return 'attachment bubbled';
|
||||
}
|
||||
return 'attachment bubbled';
|
||||
},
|
||||
initialize(options) {
|
||||
this.blob = new Blob([this.model.data], { type: this.model.contentType });
|
||||
if (!this.model.size) {
|
||||
this.model.size = this.model.data.byteLength;
|
||||
}
|
||||
if (options.timestamp) {
|
||||
this.timestamp = options.timestamp;
|
||||
}
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.blob = new Blob([this.model.data], {type: this.model.contentType});
|
||||
if (!this.model.size) {
|
||||
this.model.size = this.model.data.byteLength;
|
||||
}
|
||||
if (options.timestamp) {
|
||||
this.timestamp = options.timestamp;
|
||||
}
|
||||
},
|
||||
events: {
|
||||
'click': 'onclick'
|
||||
click: 'onclick',
|
||||
},
|
||||
unload: function() {
|
||||
this.blob = null;
|
||||
unload() {
|
||||
this.blob = null;
|
||||
|
||||
if (this.lightBoxView) {
|
||||
this.lightBoxView.remove();
|
||||
}
|
||||
if (this.fileView) {
|
||||
this.fileView.remove();
|
||||
}
|
||||
if (this.view) {
|
||||
this.view.remove();
|
||||
}
|
||||
if (this.lightBoxView) {
|
||||
this.lightBoxView.remove();
|
||||
}
|
||||
if (this.fileView) {
|
||||
this.fileView.remove();
|
||||
}
|
||||
if (this.view) {
|
||||
this.view.remove();
|
||||
}
|
||||
|
||||
this.remove();
|
||||
this.remove();
|
||||
},
|
||||
getFileType: function() {
|
||||
switch(this.model.contentType) {
|
||||
case 'video/quicktime': return 'mov';
|
||||
default: return this.model.contentType.split('/')[1];
|
||||
}
|
||||
getFileType() {
|
||||
switch (this.model.contentType) {
|
||||
case 'video/quicktime': return 'mov';
|
||||
default: return this.model.contentType.split('/')[1];
|
||||
}
|
||||
},
|
||||
onclick: function(e) {
|
||||
if (this.isImage()) {
|
||||
this.lightBoxView = new Whisper.LightboxView({ model: this });
|
||||
this.lightBoxView.render();
|
||||
this.lightBoxView.$el.appendTo(this.el);
|
||||
this.lightBoxView.$el.trigger('show');
|
||||
onclick() {
|
||||
if (this.isImage()) {
|
||||
this.lightBoxView = new Whisper.LightboxView({ model: this });
|
||||
this.lightBoxView.render();
|
||||
this.lightBoxView.$el.appendTo(this.el);
|
||||
this.lightBoxView.$el.trigger('show');
|
||||
} else {
|
||||
this.saveFile();
|
||||
}
|
||||
},
|
||||
isVoiceMessage() {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} else {
|
||||
this.saveFile();
|
||||
}
|
||||
},
|
||||
isVoiceMessage: function() {
|
||||
if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) {
|
||||
return true;
|
||||
}
|
||||
// Support for android legacy voice messages
|
||||
if (this.isAudio() && this.model.fileName === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Support for android legacy voice messages
|
||||
if (this.isAudio() && this.model.fileName === null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isAudio: function() {
|
||||
return this.model.contentType.startsWith('audio/');
|
||||
isAudio() {
|
||||
return this.model.contentType.startsWith('audio/');
|
||||
},
|
||||
isVideo: function() {
|
||||
return this.model.contentType.startsWith('video/');
|
||||
isVideo() {
|
||||
return this.model.contentType.startsWith('video/');
|
||||
},
|
||||
isImage: function() {
|
||||
var type = this.model.contentType;
|
||||
return type.startsWith('image/') && type !== 'image/tiff';
|
||||
isImage() {
|
||||
const type = this.model.contentType;
|
||||
return type.startsWith('image/') && type !== 'image/tiff';
|
||||
},
|
||||
mediaType: function() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return 'voice';
|
||||
} else if (this.isAudio()) {
|
||||
return 'audio';
|
||||
} else if (this.isVideo()) {
|
||||
return 'video';
|
||||
} else if (this.isImage()) {
|
||||
return 'image';
|
||||
}
|
||||
},
|
||||
displayName: function() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return i18n('voiceMessage');
|
||||
}
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
if (this.isAudio() || this.isVideo()) {
|
||||
return i18n('mediaMessage');
|
||||
}
|
||||
mediaType() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return 'voice';
|
||||
} else if (this.isAudio()) {
|
||||
return 'audio';
|
||||
} else if (this.isVideo()) {
|
||||
return 'video';
|
||||
} else if (this.isImage()) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
return i18n('unnamedFile');
|
||||
// NOTE: The existing code had no `return` but ESLint insists. Thought
|
||||
// about throwing an error assuming this was unreachable code but it turns
|
||||
// out that content type `image/tiff` falls through here:
|
||||
return undefined;
|
||||
},
|
||||
suggestedName: function() {
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
displayName() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return i18n('voiceMessage');
|
||||
}
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
if (this.isAudio() || this.isVideo()) {
|
||||
return i18n('mediaMessage');
|
||||
}
|
||||
|
||||
var suggestion = 'signal';
|
||||
if (this.timestamp) {
|
||||
suggestion += moment(this.timestamp).format('-YYYY-MM-DD-HHmmss');
|
||||
}
|
||||
var fileType = this.getFileType();
|
||||
if (fileType) {
|
||||
suggestion += '.' + fileType;
|
||||
}
|
||||
return suggestion;
|
||||
return i18n('unnamedFile');
|
||||
},
|
||||
saveFile: function() {
|
||||
var url = window.URL.createObjectURL(this.blob, { type: 'octet/stream' });
|
||||
var a = $('<a>').attr({ href: url, download: this.suggestedName() });
|
||||
a[0].click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
render: function() {
|
||||
if (!this.isImage()) {
|
||||
this.renderFileView();
|
||||
}
|
||||
var View;
|
||||
if (this.isImage()) {
|
||||
View = ImageView;
|
||||
} else if (this.isAudio()) {
|
||||
View = AudioView;
|
||||
} else if (this.isVideo()) {
|
||||
View = VideoView;
|
||||
}
|
||||
suggestedName() {
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
|
||||
if (!View || _.contains(UnsupportedFileTypes, this.model.contentType)) {
|
||||
this.update();
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.objectUrl) {
|
||||
this.objectUrl = window.URL.createObjectURL(this.blob);
|
||||
}
|
||||
this.view = new View(this.objectUrl, this.model.contentType);
|
||||
this.view.$el.appendTo(this.$el);
|
||||
this.listenTo(this.view, 'update', this.update);
|
||||
this.view.render();
|
||||
if (View !== ImageView) {
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 5000);
|
||||
}
|
||||
return this;
|
||||
let suggestion = 'signal';
|
||||
if (this.timestamp) {
|
||||
suggestion += moment(this.timestamp).format('-YYYY-MM-DD-HHmmss');
|
||||
}
|
||||
const fileType = this.getFileType();
|
||||
if (fileType) {
|
||||
suggestion += `.${fileType}`;
|
||||
}
|
||||
return suggestion;
|
||||
},
|
||||
onTimeout: function() {
|
||||
// Image or media element failed to load. Fall back to FileView.
|
||||
this.stopListening(this.view);
|
||||
saveFile() {
|
||||
const url = window.URL.createObjectURL(this.blob, { type: 'octet/stream' });
|
||||
const a = $('<a>').attr({ href: url, download: this.suggestedName() });
|
||||
a[0].click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
render() {
|
||||
if (!this.isImage()) {
|
||||
this.renderFileView();
|
||||
}
|
||||
let View;
|
||||
if (this.isImage()) {
|
||||
View = ImageView;
|
||||
} else if (this.isAudio()) {
|
||||
View = AudioView;
|
||||
} else if (this.isVideo()) {
|
||||
View = VideoView;
|
||||
}
|
||||
|
||||
if (!View || _.contains(UnsupportedFileTypes, this.model.contentType)) {
|
||||
this.update();
|
||||
},
|
||||
renderFileView: function() {
|
||||
this.fileView = new FileView({
|
||||
model: {
|
||||
mediaType: this.mediaType(),
|
||||
fileName: this.displayName(),
|
||||
fileSize: window.filesize(this.model.size),
|
||||
altText: i18n('clickToSave')
|
||||
}
|
||||
});
|
||||
|
||||
this.fileView.$el.appendTo(this.$el.empty());
|
||||
this.fileView.render();
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.objectUrl) {
|
||||
this.objectUrl = window.URL.createObjectURL(this.blob);
|
||||
}
|
||||
|
||||
const { blob } = this;
|
||||
const { contentType } = this.model;
|
||||
this.view = new View(this.objectUrl, { blob, contentType });
|
||||
this.view.$el.appendTo(this.$el);
|
||||
this.listenTo(this.view, 'update', this.update);
|
||||
this.view.render();
|
||||
if (View !== ImageView) {
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 5000);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
onTimeout() {
|
||||
// Image or media element failed to load. Fall back to FileView.
|
||||
this.stopListening(this.view);
|
||||
this.update();
|
||||
},
|
||||
renderFileView() {
|
||||
this.fileView = new FileView({
|
||||
model: {
|
||||
mediaType: this.mediaType(),
|
||||
fileName: this.displayName(),
|
||||
fileSize: window.filesize(this.model.size),
|
||||
altText: i18n('clickToSave'),
|
||||
},
|
||||
});
|
||||
|
||||
this.fileView.$el.appendTo(this.$el.empty());
|
||||
this.fileView.render();
|
||||
return this;
|
||||
},
|
||||
update() {
|
||||
clearTimeout(this.timeout);
|
||||
this.trigger('update');
|
||||
},
|
||||
update: function() {
|
||||
clearTimeout(this.timeout);
|
||||
this.trigger('update');
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.LightboxView = Whisper.View.extend({
|
||||
templateName: 'lightbox',
|
||||
className: 'modal lightbox',
|
||||
initialize: function() {
|
||||
this.window = window;
|
||||
this.$document = $(this.window.document);
|
||||
this.listener = this.onkeyup.bind(this);
|
||||
this.$document.on('keyup', this.listener);
|
||||
},
|
||||
events: {
|
||||
'click .save': 'save',
|
||||
'click .close': 'remove',
|
||||
'click': 'onclick'
|
||||
},
|
||||
save: function(e) {
|
||||
this.model.saveFile();
|
||||
},
|
||||
onclick: function(e) {
|
||||
var $el = this.$(e.target);
|
||||
if (!$el.hasClass('image') && !$el.closest('.controls').length ) {
|
||||
e.preventDefault();
|
||||
this.remove();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onkeyup: function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.remove();
|
||||
this.$document.off('keyup', this.listener);
|
||||
}
|
||||
},
|
||||
render_attributes: function() {
|
||||
return { url: this.model.objectUrl };
|
||||
templateName: 'lightbox',
|
||||
className: 'modal lightbox',
|
||||
initialize() {
|
||||
this.window = window;
|
||||
this.$document = $(this.window.document);
|
||||
this.listener = this.onkeyup.bind(this);
|
||||
this.$document.on('keyup', this.listener);
|
||||
},
|
||||
events: {
|
||||
'click .save': 'save',
|
||||
'click .close': 'remove',
|
||||
click: 'onclick',
|
||||
},
|
||||
save() {
|
||||
this.model.saveFile();
|
||||
},
|
||||
onclick(e) {
|
||||
const $el = this.$(e.target);
|
||||
if (!$el.hasClass('image') && !$el.closest('.controls').length) {
|
||||
e.preventDefault();
|
||||
this.remove();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
return true;
|
||||
},
|
||||
onkeyup(e) {
|
||||
if (e.keyCode === ESCAPE_KEY_CODE) {
|
||||
this.remove();
|
||||
this.$document.off('keyup', this.listener);
|
||||
}
|
||||
},
|
||||
render_attributes() {
|
||||
return { url: this.model.objectUrl };
|
||||
},
|
||||
});
|
||||
}());
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
/* eslint-disable */
|
||||
|
||||
/* global textsecure: false */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { MIME } = window.Signal.Types;
|
||||
|
||||
Whisper.FileSizeToast = Whisper.ToastView.extend({
|
||||
templateName: 'file-size-modal',
|
||||
render_attributes: function() {
|
||||
|
@ -30,6 +33,7 @@
|
|||
this.thumb = new Whisper.AttachmentPreviewView();
|
||||
this.$el.addClass('file-input');
|
||||
this.window = options.window;
|
||||
this.previewObjectUrl = null;
|
||||
},
|
||||
|
||||
events: {
|
||||
|
@ -93,7 +97,6 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// loadImage.scale -> components/blueimp-load-image
|
||||
var canvas = loadImage.scale(img, {
|
||||
canvas: true, maxWidth: maxWidth, maxHeight: maxHeight
|
||||
});
|
||||
|
@ -103,11 +106,13 @@
|
|||
var blob;
|
||||
do {
|
||||
i = i - 1;
|
||||
// dataURLtoBlob -> components/blueimp-canvas-to-blob
|
||||
blob = dataURLtoBlob(
|
||||
blob = window.dataURLToBlobSync(
|
||||
canvas.toDataURL('image/jpeg', quality)
|
||||
);
|
||||
quality = quality * maxSize / blob.size;
|
||||
// NOTE: During testing with a large image, we observed the
|
||||
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
|
||||
if (quality < 0.5) {
|
||||
quality = 0.5;
|
||||
}
|
||||
|
@ -132,13 +137,25 @@
|
|||
case 'audio': this.addThumb('images/audio.svg'); break;
|
||||
case 'video': this.addThumb('images/video.svg'); break;
|
||||
case 'image':
|
||||
this.oUrl = URL.createObjectURL(file);
|
||||
this.addThumb(this.oUrl);
|
||||
if (!MIME.isJPEG(file.type)) {
|
||||
this.previewObjectUrl = URL.createObjectURL(file);
|
||||
this.addThumb(this.previewObjectUrl);
|
||||
break;
|
||||
}
|
||||
|
||||
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||
// to `async` / `await`:
|
||||
// eslint-disable-next-line more/no-then
|
||||
window.autoOrientImage(file)
|
||||
.then(dataURL => this.addThumb(dataURL));
|
||||
break;
|
||||
default:
|
||||
this.addThumb('images/file.svg'); break;
|
||||
}
|
||||
|
||||
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||
// to `async` / `await`:
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.autoScale(file).then(function(blob) {
|
||||
var limitKb = 1000000;
|
||||
var blobType = file.type === 'image/gif' ? 'gif' : type;
|
||||
|
@ -177,30 +194,41 @@
|
|||
return files && files.length && files.length > 0;
|
||||
},
|
||||
|
||||
getFiles: function() {
|
||||
var promises = [];
|
||||
var files = this.file ? [this.file] : this.$input.prop('files');
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
promises.push(this.getFile(files[i]));
|
||||
}
|
||||
this.clearForm();
|
||||
return Promise.all(promises);
|
||||
},
|
||||
/* eslint-enable */
|
||||
/* jshint ignore:start */
|
||||
getFiles() {
|
||||
const files = this.file ? [this.file] : Array.from(this.$input.prop('files'));
|
||||
const promise = Promise.all(files.map(file => this.getFile(file)));
|
||||
this.clearForm();
|
||||
return promise;
|
||||
},
|
||||
|
||||
getFile: function(file) {
|
||||
file = file || this.file || this.$input.prop('files')[0];
|
||||
if (file === undefined) { return Promise.resolve(); }
|
||||
var flags;
|
||||
if (this.isVoiceNote) {
|
||||
flags = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||
}
|
||||
return this.autoScale(file).then(this.readFile).then(function(attachment) {
|
||||
if (flags) {
|
||||
attachment.flags = flags;
|
||||
}
|
||||
return attachment;
|
||||
}.bind(this));
|
||||
},
|
||||
getFile(rawFile) {
|
||||
const file = rawFile || this.file || this.$input.prop('files')[0];
|
||||
if (file === undefined) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const attachmentFlags = this.isVoiceNote
|
||||
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||
: null;
|
||||
|
||||
const setFlags = flags => (attachment) => {
|
||||
const newAttachment = Object.assign({}, attachment);
|
||||
if (flags) {
|
||||
newAttachment.flags = flags;
|
||||
}
|
||||
return newAttachment;
|
||||
};
|
||||
|
||||
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||
// to `async` / `await`:
|
||||
// eslint-disable-next-line more/no-then
|
||||
return this.autoScale(file)
|
||||
.then(this.readFile)
|
||||
.then(setFlags(attachmentFlags));
|
||||
},
|
||||
/* jshint ignore:end */
|
||||
/* eslint-disable */
|
||||
|
||||
getThumbnail: function() {
|
||||
// Scale and crop an image to 256px square
|
||||
|
@ -228,8 +256,7 @@
|
|||
crop: true, minWidth: size, minHeight: size
|
||||
});
|
||||
|
||||
// dataURLtoBlob -> components/blueimp-canvas-to-blob
|
||||
var blob = dataURLtoBlob(canvas.toDataURL('image/png'));
|
||||
var blob = window.dataURLToBlobSync(canvas.toDataURL('image/png'));
|
||||
|
||||
resolve(blob);
|
||||
};
|
||||
|
@ -237,6 +264,7 @@
|
|||
}).then(this.readFile);
|
||||
},
|
||||
|
||||
// File -> Promise Attachment
|
||||
readFile: function(file) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var FR = new FileReader();
|
||||
|
@ -255,10 +283,11 @@
|
|||
},
|
||||
|
||||
clearForm: function() {
|
||||
if (this.oUrl) {
|
||||
URL.revokeObjectURL(this.oUrl);
|
||||
this.oUrl = null;
|
||||
if (this.previewObjectUrl) {
|
||||
URL.revokeObjectURL(this.previewObjectUrl);
|
||||
this.previewObjectUrl = null;
|
||||
}
|
||||
|
||||
this.thumb.remove();
|
||||
this.$('.avatar').show();
|
||||
this.$el.trigger('force-resize');
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
var State = {
|
||||
IMPORTING: 1,
|
||||
COMPLETE: 2
|
||||
COMPLETE: 2,
|
||||
LIGHT_COMPLETE: 3,
|
||||
};
|
||||
|
||||
var IMPORT_STARTED = 'importStarted';
|
||||
|
@ -39,12 +40,13 @@
|
|||
};
|
||||
|
||||
Whisper.ImportView = Whisper.View.extend({
|
||||
templateName: 'app-migration-screen',
|
||||
className: 'app-loading-screen',
|
||||
templateName: 'import-flow-template',
|
||||
className: 'full-screen-flow',
|
||||
events: {
|
||||
'click .import': 'onImport',
|
||||
'click .choose': 'onImport',
|
||||
'click .restart': 'onRestart',
|
||||
'click .cancel': 'onCancel',
|
||||
'click .register': 'onRegister',
|
||||
},
|
||||
initialize: function() {
|
||||
if (Whisper.Import.isIncomplete()) {
|
||||
|
@ -55,41 +57,42 @@
|
|||
this.pending = Promise.resolve();
|
||||
},
|
||||
render_attributes: function() {
|
||||
var message;
|
||||
var importButton;
|
||||
var hideProgress = true;
|
||||
var restartButton;
|
||||
var cancelButton;
|
||||
|
||||
if (this.error) {
|
||||
return {
|
||||
message: i18n('importError'),
|
||||
hideProgress: true,
|
||||
importButton: i18n('tryAgain'),
|
||||
isError: true,
|
||||
errorHeader: i18n('importErrorHeader'),
|
||||
errorMessage: i18n('importError'),
|
||||
chooseButton: i18n('importAgain'),
|
||||
};
|
||||
}
|
||||
|
||||
switch (this.state) {
|
||||
case State.COMPLETE:
|
||||
message = i18n('importComplete');
|
||||
restartButton = i18n('restartSignal');
|
||||
break;
|
||||
case State.IMPORTING:
|
||||
message = i18n('importing');
|
||||
hideProgress = false;
|
||||
break;
|
||||
default:
|
||||
message = i18n('importInstructions');
|
||||
importButton = i18n('chooseDirectory');
|
||||
cancelButton = i18n('cancel');
|
||||
var restartButton = i18n('importCompleteStartButton');
|
||||
var registerButton = i18n('importCompleteLinkButton');
|
||||
var step = 'step2';
|
||||
|
||||
if (this.state === State.IMPORTING) {
|
||||
step = 'step3';
|
||||
} else if (this.state === State.COMPLETE) {
|
||||
registerButton = null;
|
||||
step = 'step4';
|
||||
} else if (this.state === State.LIGHT_COMPLETE) {
|
||||
restartButton = null;
|
||||
step = 'step4';
|
||||
}
|
||||
|
||||
return {
|
||||
hideProgress: hideProgress,
|
||||
message: message,
|
||||
importButton: importButton,
|
||||
isStep2: step === 'step2',
|
||||
chooseHeader: i18n('loadDataHeader'),
|
||||
choose: i18n('loadDataDescription'),
|
||||
chooseButton: i18n('chooseDirectory'),
|
||||
|
||||
isStep3: step === 'step3',
|
||||
importingHeader: i18n('importingHeader'),
|
||||
|
||||
isStep4: step === 'step4',
|
||||
completeHeader: i18n('importCompleteHeader'),
|
||||
restartButton: restartButton,
|
||||
cancelButton: cancelButton,
|
||||
registerButton: registerButton,
|
||||
};
|
||||
},
|
||||
onRestart: function() {
|
||||
|
@ -110,9 +113,16 @@
|
|||
}
|
||||
});
|
||||
},
|
||||
doImport: function(directory) {
|
||||
this.error = null;
|
||||
onRegister: function() {
|
||||
// AppView listens for this, and opens up InstallView to the QR code step to
|
||||
// finish setting this device up.
|
||||
this.trigger('light-import');
|
||||
},
|
||||
|
||||
doImport: function(directory) {
|
||||
window.removeSetupMenuItems();
|
||||
|
||||
this.error = null;
|
||||
this.state = State.IMPORTING;
|
||||
this.render();
|
||||
|
||||
|
@ -125,25 +135,17 @@
|
|||
Whisper.Import.start(),
|
||||
Whisper.Backup.importFromDirectory(directory)
|
||||
]);
|
||||
}).then(function() {
|
||||
// Catching in-memory cache up with what's in indexeddb now...
|
||||
// NOTE: this fires storage.onready, listened to across the app. We'll restart
|
||||
// to complete the install to start up cleanly with everything now in the DB.
|
||||
return storage.fetch();
|
||||
}).then(function() {
|
||||
return Promise.all([
|
||||
// Clearing any migration-related state inherited from the Chrome App
|
||||
storage.remove('migrationState'),
|
||||
storage.remove('migrationEnabled'),
|
||||
storage.remove('migrationEverCompleted'),
|
||||
storage.remove('migrationStorageLocation'),
|
||||
}).then(function(results) {
|
||||
var importResult = results[1];
|
||||
|
||||
Whisper.Import.saveLocation(directory),
|
||||
Whisper.Import.complete()
|
||||
]);
|
||||
}).then(function() {
|
||||
this.state = State.COMPLETE;
|
||||
this.render();
|
||||
// A full import changes so much we need a restart of the app
|
||||
if (importResult.fullImport) {
|
||||
return this.finishFullImport(directory);
|
||||
}
|
||||
|
||||
// A light import just brings in contacts, groups, and messages. And we need a
|
||||
// normal link to finish the process.
|
||||
return this.finishLightImport(directory);
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.log('Error importing:', error && error.stack ? error.stack : error);
|
||||
|
||||
|
@ -153,6 +155,40 @@
|
|||
|
||||
return Whisper.Backup.clearDatabase();
|
||||
}.bind(this));
|
||||
},
|
||||
finishLightImport: function(directory) {
|
||||
ConversationController.reset();
|
||||
|
||||
return ConversationController.load().then(function() {
|
||||
return Promise.all([
|
||||
Whisper.Import.saveLocation(directory),
|
||||
Whisper.Import.complete(),
|
||||
]);
|
||||
}).then(function() {
|
||||
this.state = State.LIGHT_COMPLETE;
|
||||
this.render();
|
||||
}.bind(this));
|
||||
},
|
||||
finishFullImport: function(directory) {
|
||||
// Catching in-memory cache up with what's in indexeddb now...
|
||||
// NOTE: this fires storage.onready, listened to across the app. We'll restart
|
||||
// to complete the install to start up cleanly with everything now in the DB.
|
||||
return storage.fetch()
|
||||
.then(function() {
|
||||
return Promise.all([
|
||||
// Clearing any migration-related state inherited from the Chrome App
|
||||
storage.remove('migrationState'),
|
||||
storage.remove('migrationEnabled'),
|
||||
storage.remove('migrationEverCompleted'),
|
||||
storage.remove('migrationStorageLocation'),
|
||||
|
||||
Whisper.Import.saveLocation(directory),
|
||||
Whisper.Import.complete()
|
||||
]);
|
||||
}).then(function() {
|
||||
this.state = State.COMPLETE;
|
||||
this.render();
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -14,145 +14,176 @@
|
|||
NETWORK_ERROR: 'NetworkError',
|
||||
};
|
||||
|
||||
var DEVICE_NAME_SELECTOR = 'input.device-name';
|
||||
var CONNECTION_ERROR = -1;
|
||||
var TOO_MANY_DEVICES = 411;
|
||||
|
||||
Whisper.InstallView = Whisper.View.extend({
|
||||
templateName: 'install_flow_template',
|
||||
className: 'main install',
|
||||
render_attributes: function() {
|
||||
var twitterHref = 'https://twitter.com/whispersystems';
|
||||
var signalHref = 'https://signal.org/install';
|
||||
return {
|
||||
installWelcome: i18n('installWelcome'),
|
||||
installTagline: i18n('installTagline'),
|
||||
installGetStartedButton: i18n('installGetStartedButton'),
|
||||
installSignalLink: this.i18n_with_links('installSignalLink', signalHref),
|
||||
installIHaveSignalButton: i18n('installGotIt'),
|
||||
installFollowUs: this.i18n_with_links('installFollowUs', twitterHref),
|
||||
installAndroidInstructions: i18n('installAndroidInstructions'),
|
||||
installLinkingWithNumber: i18n('installLinkingWithNumber'),
|
||||
installComputerName: i18n('installComputerName'),
|
||||
installFinalButton: i18n('installFinalButton'),
|
||||
installTooManyDevices: i18n('installTooManyDevices'),
|
||||
installConnectionFailed: i18n('installConnectionFailed'),
|
||||
ok: i18n('ok'),
|
||||
tryAgain: i18n('tryAgain'),
|
||||
development: window.config.environment === 'development'
|
||||
};
|
||||
templateName: 'link-flow-template',
|
||||
className: 'main full-screen-flow',
|
||||
events: {
|
||||
'click .try-again': 'connect',
|
||||
// handler for finish button is in confirmNumber()
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.counter = 0;
|
||||
options = options || {};
|
||||
|
||||
this.render();
|
||||
|
||||
var deviceName = textsecure.storage.user.getDeviceName();
|
||||
if (!deviceName) {
|
||||
deviceName = window.config.hostname;
|
||||
}
|
||||
|
||||
this.$('#device-name').val(deviceName);
|
||||
this.selectStep(Steps.INSTALL_SIGNAL);
|
||||
this.selectStep(Steps.SCAN_QR_CODE);
|
||||
this.connect();
|
||||
this.on('disconnected', this.reconnect);
|
||||
|
||||
if (Whisper.Registration.everDone()) {
|
||||
this.selectStep(Steps.SCAN_QR_CODE);
|
||||
this.hideDots();
|
||||
// Keep data around if it's a re-link, or the middle of a light import
|
||||
this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData;
|
||||
},
|
||||
render_attributes: function() {
|
||||
var errorMessage;
|
||||
|
||||
if (this.error) {
|
||||
if (this.error.name === 'HTTPError'
|
||||
&& this.error.code == TOO_MANY_DEVICES) {
|
||||
|
||||
errorMessage = i18n('installTooManyDevices');
|
||||
}
|
||||
else if (this.error.name === 'HTTPError'
|
||||
&& this.error.code == CONNECTION_ERROR) {
|
||||
|
||||
errorMessage = i18n('installConnectionFailed');
|
||||
}
|
||||
else if (this.error.message === 'websocket closed') {
|
||||
// AccountManager.registerSecondDevice uses this specific
|
||||
// 'websocket closed' error message
|
||||
errorMessage = i18n('installConnectionFailed');
|
||||
}
|
||||
|
||||
return {
|
||||
isError: true,
|
||||
errorHeader: 'Something went wrong!',
|
||||
errorMessage,
|
||||
errorButton: 'Try again',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isStep3: this.step === Steps.SCAN_QR_CODE,
|
||||
linkYourPhone: i18n('linkYourPhone'),
|
||||
signalSettings: i18n('signalSettings'),
|
||||
linkedDevices: i18n('linkedDevices'),
|
||||
androidFinalStep: i18n('plusButton'),
|
||||
appleFinalStep: i18n('linkNewDevice'),
|
||||
|
||||
isStep4: this.step === Steps.ENTER_NAME,
|
||||
chooseName: i18n('chooseDeviceName'),
|
||||
finishLinkingPhoneButton: i18n('finishLinkingPhone'),
|
||||
|
||||
isStep5: this.step === Steps.PROGRESS_BAR,
|
||||
syncing: i18n('initialSync'),
|
||||
};
|
||||
},
|
||||
selectStep: function(step) {
|
||||
this.step = step;
|
||||
this.render();
|
||||
},
|
||||
connect: function() {
|
||||
this.error = null;
|
||||
this.selectStep(Steps.SCAN_QR_CODE);
|
||||
this.clearQR();
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
|
||||
var accountManager = getAccountManager();
|
||||
|
||||
accountManager.registerSecondDevice(
|
||||
this.setProvisioningUrl.bind(this),
|
||||
this.confirmNumber.bind(this),
|
||||
this.incrementCounter.bind(this)
|
||||
this.confirmNumber.bind(this)
|
||||
).catch(this.handleDisconnect.bind(this));
|
||||
},
|
||||
handleDisconnect: function(e) {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
console.log('provisioning failed', e.stack);
|
||||
|
||||
this.error = e;
|
||||
this.render();
|
||||
|
||||
if (e.message === 'websocket closed') {
|
||||
this.showConnectionError();
|
||||
this.trigger('disconnected');
|
||||
} else if (e.name === 'HTTPError' && e.code == -1) {
|
||||
this.selectStep(Steps.NETWORK_ERROR);
|
||||
} else if (e.name === 'HTTPError' && e.code == 411) {
|
||||
this.showTooManyDevices();
|
||||
} else {
|
||||
} else if (e.name !== 'HTTPError'
|
||||
|| (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) {
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
reconnect: function() {
|
||||
setTimeout(this.connect.bind(this), 10000);
|
||||
},
|
||||
events: function() {
|
||||
return {
|
||||
'click .error-dialog .ok': 'connect',
|
||||
'click .step1': 'onCancel',
|
||||
'click .step2': this.selectStep.bind(this, Steps.INSTALL_SIGNAL),
|
||||
'click .step3': this.selectStep.bind(this, Steps.SCAN_QR_CODE)
|
||||
};
|
||||
},
|
||||
onCancel: function() {
|
||||
this.canceled = true;
|
||||
this.trigger('cancel');
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
this.timeout = setTimeout(this.connect.bind(this), 10000);
|
||||
},
|
||||
clearQR: function() {
|
||||
this.$('#qr').text(i18n("installConnecting"));
|
||||
this.$('#qr img').remove();
|
||||
this.$('#qr canvas').remove();
|
||||
this.$('#qr .container').show();
|
||||
this.$('#qr').removeClass('ready');
|
||||
},
|
||||
setProvisioningUrl: function(url) {
|
||||
this.$('#qr').html('');
|
||||
new QRCode(this.$('#qr')[0]).makeCode(url);
|
||||
if ($('#qr').length === 0) {
|
||||
console.log('Did not find #qr element in the DOM!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$('#qr .container').hide();
|
||||
this.qr = new QRCode(this.$('#qr')[0]).makeCode(url);
|
||||
this.$('#qr').removeAttr('title');
|
||||
this.$('#qr').addClass('ready');
|
||||
},
|
||||
setDeviceNameDefault: function() {
|
||||
var deviceName = textsecure.storage.user.getDeviceName();
|
||||
|
||||
this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.config.hostname);
|
||||
this.$(DEVICE_NAME_SELECTOR).focus();
|
||||
},
|
||||
confirmNumber: function(number) {
|
||||
var parsed = libphonenumber.parse(number);
|
||||
var stepId = '#step' + Steps.ENTER_NAME;
|
||||
this.$(stepId + ' .number').text(libphonenumber.format(
|
||||
parsed,
|
||||
libphonenumber.PhoneNumberFormat.INTERNATIONAL
|
||||
));
|
||||
window.removeSetupMenuItems();
|
||||
this.selectStep(Steps.ENTER_NAME);
|
||||
this.$('#device-name').focus();
|
||||
this.setDeviceNameDefault();
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
this.$(stepId + ' .cancel').click(function(e) {
|
||||
reject();
|
||||
});
|
||||
this.$(stepId).submit(function(e) {
|
||||
this.$('.finish').click(function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
var name = this.$('#device-name').val();
|
||||
|
||||
var name = this.$(DEVICE_NAME_SELECTOR).val();
|
||||
name = name.replace(/\0/g,''); // strip unicode null
|
||||
if (name.trim().length === 0) {
|
||||
this.$('#device-name').focus();
|
||||
this.$(DEVICE_NAME_SELECTOR).focus();
|
||||
return;
|
||||
}
|
||||
this.$('.progress-dialog .status').text(i18n('installGeneratingKeys'));
|
||||
|
||||
this.selectStep(Steps.PROGRESS_BAR);
|
||||
resolve(name);
|
||||
|
||||
var finish = function() {
|
||||
resolve(name);
|
||||
};
|
||||
|
||||
// Delete all data from database unless we're in the middle
|
||||
// of a re-link, or we are finishing a light import. Without this,
|
||||
// app restarts at certain times can cause weird things to happen,
|
||||
// like data from a previous incomplete light import showing up
|
||||
// after a new install.
|
||||
if (this.shouldRetainData) {
|
||||
return finish();
|
||||
}
|
||||
|
||||
Whisper.Backup.clearDatabase().then(finish, function(error) {
|
||||
console.log(
|
||||
'confirmNumber: error clearing database',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
finish();
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
incrementCounter: function() {
|
||||
this.$('.progress-dialog .bar').css('width', (++this.counter * 100 / 100) + '%');
|
||||
},
|
||||
selectStep: function(step) {
|
||||
this.$('.step').hide();
|
||||
this.$('#step' + step).show();
|
||||
},
|
||||
showSync: function() {
|
||||
this.$('.progress-dialog .status').text(i18n('installSyncingGroupsAndContacts'));
|
||||
this.$('.progress-dialog .bar').addClass('progress-bar-striped active');
|
||||
},
|
||||
showTooManyDevices: function() {
|
||||
this.selectStep(Steps.TOO_MANY_DEVICES);
|
||||
},
|
||||
showConnectionError: function() {
|
||||
this.$('#qr').text(i18n("installConnectionFailed"));
|
||||
},
|
||||
hideDots: function() {
|
||||
this.$('#step' + Steps.SCAN_QR_CODE + ' .nav .dot').hide();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -82,13 +82,19 @@
|
|||
render_attributes: function() {
|
||||
var seconds = this.model.get('expirationTimerUpdate').expireTimer;
|
||||
var timerMessage;
|
||||
if (this.conversation.id === textsecure.storage.user.getNumber()) {
|
||||
timerMessage = i18n('youChangedTheTimer',
|
||||
Whisper.ExpirationTimerOptions.getName(seconds));
|
||||
|
||||
var timerUpdate = this.model.get('expirationTimerUpdate');
|
||||
var prettySeconds = Whisper.ExpirationTimerOptions.getName(seconds);
|
||||
|
||||
if (timerUpdate && timerUpdate.fromSync) {
|
||||
timerMessage = i18n('timerSetOnSync', prettySeconds);
|
||||
} else if (this.conversation.id === textsecure.storage.user.getNumber()) {
|
||||
timerMessage = i18n('youChangedTheTimer', prettySeconds);
|
||||
} else {
|
||||
timerMessage = i18n('theyChangedTheTimer', [
|
||||
this.conversation.getTitle(),
|
||||
Whisper.ExpirationTimerOptions.getName(seconds)]);
|
||||
prettySeconds,
|
||||
]);
|
||||
}
|
||||
return { content: timerMessage };
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
className: 'settings modal expand',
|
||||
templateName: 'settings',
|
||||
initialize: function() {
|
||||
this.deviceName = textsecure.storage.user.getDeviceName();
|
||||
this.render();
|
||||
new RadioButtonGroupView({
|
||||
el: this.$('.notification-settings'),
|
||||
|
@ -88,6 +89,8 @@
|
|||
},
|
||||
render_attributes: function() {
|
||||
return {
|
||||
deviceNameLabel: i18n('deviceName'),
|
||||
deviceName: this.deviceName,
|
||||
theme: i18n('theme'),
|
||||
notifications: i18n('notifications'),
|
||||
notificationSettingsDialog: i18n('notificationSettingsDialog'),
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
Whisper.StandaloneRegistrationView = Whisper.View.extend({
|
||||
templateName: 'standalone',
|
||||
className: 'install main',
|
||||
className: 'full-screen-flow',
|
||||
initialize: function() {
|
||||
this.accountManager = getAccountManager();
|
||||
|
||||
|
@ -21,16 +21,15 @@
|
|||
this.$('#error').hide();
|
||||
},
|
||||
events: {
|
||||
'submit #form': 'submit',
|
||||
'validation input.number': 'onValidation',
|
||||
'change #code': 'onChangeCode',
|
||||
'click #request-voice': 'requestVoice',
|
||||
'click #request-sms': 'requestSMSVerification',
|
||||
'change #code': 'onChangeCode',
|
||||
'click #verifyCode': 'verifyCode',
|
||||
},
|
||||
submit: function(e) {
|
||||
e.preventDefault();
|
||||
verifyCode: function(e) {
|
||||
var number = this.phoneView.validateNumber();
|
||||
var verificationCode = $('#code').val().replace(/\D+/g, "");
|
||||
var verificationCode = $('#code').val().replace(/\D+/g, '');
|
||||
|
||||
this.accountManager.registerSingleDevice(number, verificationCode).then(function() {
|
||||
this.$el.trigger('openInbox');
|
||||
|
@ -64,6 +63,7 @@
|
|||
}
|
||||
},
|
||||
requestVoice: function() {
|
||||
window.removeSetupMenuItems();
|
||||
this.$('#error').hide();
|
||||
var number = this.phoneView.validateNumber();
|
||||
if (number) {
|
||||
|
@ -74,6 +74,7 @@
|
|||
}
|
||||
},
|
||||
requestSMSVerification: function() {
|
||||
window.removeSetupMenuItems();
|
||||
$('#error').hide();
|
||||
var number = this.phoneView.validateNumber();
|
||||
if (number) {
|
||||
|
|
|
@ -35291,8 +35291,6 @@ var Internal = Internal || {};
|
|||
result = result | (a[i] ^ b[i]);
|
||||
}
|
||||
if (result !== 0) {
|
||||
console.log('Our MAC ', dcodeIO.ByteBuffer.wrap(calculated_mac).toHex());
|
||||
console.log('Their MAC', dcodeIO.ByteBuffer.wrap(mac).toHex());
|
||||
throw new Error("Bad MAC");
|
||||
}
|
||||
});
|
||||
|
@ -36016,14 +36014,7 @@ libsignal.SessionBuilder = function (storage, remoteAddress) {
|
|||
this.processV3 = builder.processV3.bind(builder);
|
||||
};
|
||||
|
||||
function SessionCipher(storage, remoteAddress, options) {
|
||||
options = options || {};
|
||||
|
||||
if (typeof options.messageKeysLimit === 'undefined') {
|
||||
options.messageKeysLimit = 1000;
|
||||
}
|
||||
|
||||
this.messageKeysLimit = options.messageKeysLimit;
|
||||
function SessionCipher(storage, remoteAddress) {
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.storage = storage;
|
||||
}
|
||||
|
@ -36296,15 +36287,14 @@ SessionCipher.prototype = {
|
|||
});
|
||||
},
|
||||
fillMessageKeys: function(chain, counter) {
|
||||
if (this.messageKeysLimit && Object.keys(chain.messageKeys).length >= this.messageKeysLimit) {
|
||||
console.log("Too many message keys for chain");
|
||||
return Promise.resolve(); // Stalker, much?
|
||||
}
|
||||
|
||||
if (chain.chainKey.counter >= counter) {
|
||||
return Promise.resolve(); // Already calculated
|
||||
}
|
||||
|
||||
if (counter - chain.chainKey.counter > 2000) {
|
||||
throw new Error('Over 2000 messages into the future!');
|
||||
}
|
||||
|
||||
if (chain.chainKey.key === undefined) {
|
||||
throw new Error("Got invalid request to extend chain after it was already closed");
|
||||
}
|
||||
|
@ -36433,8 +36423,8 @@ SessionCipher.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
libsignal.SessionCipher = function(storage, remoteAddress, options) {
|
||||
var cipher = new SessionCipher(storage, remoteAddress, options);
|
||||
libsignal.SessionCipher = function(storage, remoteAddress) {
|
||||
var cipher = new SessionCipher(storage, remoteAddress);
|
||||
|
||||
// returns a Promise that resolves to a ciphertext object
|
||||
this.encrypt = cipher.encrypt.bind(cipher);
|
||||
|
|
|
@ -558,23 +558,23 @@ MessageReceiver.prototype.extend({
|
|||
handleReceiptMessage: function(envelope, receiptMessage) {
|
||||
var results = [];
|
||||
if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY) {
|
||||
for (var i = 0; i < receiptMessage.timestamps.length; ++i) {
|
||||
for (var i = 0; i < receiptMessage.timestamp.length; ++i) {
|
||||
var ev = new Event('delivery');
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.deliveryReceipt = {
|
||||
timestamp : receiptMessage.timestamps[i].toNumber(),
|
||||
timestamp : receiptMessage.timestamp[i].toNumber(),
|
||||
source : envelope.source,
|
||||
sourceDevice : envelope.sourceDevice
|
||||
};
|
||||
results.push(this.dispatchAndWait(ev));
|
||||
}
|
||||
} else if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ) {
|
||||
for (var i = 0; i < receiptMessage.timestamps.length; ++i) {
|
||||
for (var i = 0; i < receiptMessage.timestamp.length; ++i) {
|
||||
var ev = new Event('read');
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.timestamp = envelope.timestamp.toNumber();
|
||||
ev.read = {
|
||||
timestamp : receiptMessage.timestamps[i].toNumber(),
|
||||
timestamp : receiptMessage.timestamp[i].toNumber(),
|
||||
reader : envelope.source
|
||||
}
|
||||
results.push(this.dispatchAndWait(ev));
|
||||
|
@ -626,17 +626,17 @@ MessageReceiver.prototype.extend({
|
|||
return this.handleRead(envelope, syncMessage.read);
|
||||
} else if (syncMessage.verified) {
|
||||
return this.handleVerified(envelope, syncMessage.verified);
|
||||
} else if (syncMessage.settings) {
|
||||
return this.handleSettings(envelope, syncMessage.settings);
|
||||
} else if (syncMessage.configuration) {
|
||||
return this.handleConfiguration(envelope, syncMessage.configuration);
|
||||
} else {
|
||||
throw new Error('Got empty SyncMessage');
|
||||
}
|
||||
},
|
||||
handleSettings: function(envelope, settings) {
|
||||
var ev = new Event('settings');
|
||||
handleConfiguration: function(envelope, configuration) {
|
||||
var ev = new Event('configuration');
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.settings = {
|
||||
readReceipts: settings.readReceipts
|
||||
ev.configuration = {
|
||||
readReceipts: configuration.readReceipts
|
||||
};
|
||||
return this.dispatchAndWait(ev);
|
||||
},
|
||||
|
@ -815,8 +815,8 @@ MessageReceiver.prototype.extend({
|
|||
|
||||
// Before June, all incoming messages were still DataMessage:
|
||||
// - iOS: Michael Kirk says that they were sending Legacy messages until June
|
||||
// - Desktop: https://github.com/WhisperSystems/Signal-Desktop/commit/e8548879db405d9bcd78b82a456ad8d655592c0f
|
||||
// - Android: https://github.com/WhisperSystems/libsignal-service-java/commit/61a75d023fba950ff9b4c75a249d1a3408e12958
|
||||
// - Desktop: https://github.com/signalapp/Signal-Desktop/commit/e8548879db405d9bcd78b82a456ad8d655592c0f
|
||||
// - Android: https://github.com/signalapp/libsignal-service-java/commit/61a75d023fba950ff9b4c75a249d1a3408e12958
|
||||
//
|
||||
// var d = new Date('2017-06-01T07:00:00.000Z');
|
||||
// d.getTime();
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
console.log(text);
|
||||
throw error;
|
||||
}
|
||||
var protos = result.build('textsecure');
|
||||
var protos = result.build('signalservice');
|
||||
if (!protos) {
|
||||
var text = 'Error loading protos from ' + filename + ' (root: ' + window.PROTO_ROOT + ')';
|
||||
console.log(text);
|
||||
|
@ -23,7 +23,7 @@
|
|||
});
|
||||
};
|
||||
|
||||
loadProtoBufs('IncomingPushMessageSignal.proto');
|
||||
loadProtoBufs('SignalService.proto');
|
||||
loadProtoBufs('SubProtocol.proto');
|
||||
loadProtoBufs('DeviceMessages.proto');
|
||||
})();
|
||||
|
|
|
@ -388,7 +388,7 @@ MessageSender.prototype = {
|
|||
sendReadReceipts: function(sender, timestamps) {
|
||||
var receiptMessage = new textsecure.protobuf.ReceiptMessage();
|
||||
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
|
||||
receiptMessage.timestamps = timestamps;
|
||||
receiptMessage.timestamp = timestamps;
|
||||
|
||||
var contentMessage = new textsecure.protobuf.Content();
|
||||
contentMessage.receiptMessage = receiptMessage;
|
||||
|
|
|
@ -17,7 +17,7 @@ describe("ContactBuffer", function() {
|
|||
var contactInfo = new textsecure.protobuf.ContactDetails({
|
||||
name: "Zero Cool",
|
||||
number: "+10000000000",
|
||||
avatar: { contentType: "image/jpg", length: avatarLen }
|
||||
avatar: { contentType: "image/jpeg", length: avatarLen }
|
||||
});
|
||||
var contactInfoBuffer = contactInfo.encode().toArrayBuffer();
|
||||
|
||||
|
@ -41,7 +41,7 @@ describe("ContactBuffer", function() {
|
|||
count++;
|
||||
assert.strictEqual(contact.name, "Zero Cool");
|
||||
assert.strictEqual(contact.number, "+10000000000");
|
||||
assert.strictEqual(contact.avatar.contentType, "image/jpg");
|
||||
assert.strictEqual(contact.avatar.contentType, "image/jpeg");
|
||||
assert.strictEqual(contact.avatar.length, 255);
|
||||
assert.strictEqual(contact.avatar.data.byteLength, 255);
|
||||
var avatarBytes = new Uint8Array(contact.avatar.data);
|
||||
|
@ -68,7 +68,7 @@ describe("GroupBuffer", function() {
|
|||
id: new Uint8Array([1, 3, 3, 7]).buffer,
|
||||
name: "Hackers",
|
||||
members: ['cereal', 'burn', 'phreak', 'joey'],
|
||||
avatar: { contentType: "image/jpg", length: avatarLen }
|
||||
avatar: { contentType: "image/jpeg", length: avatarLen }
|
||||
});
|
||||
var groupInfoBuffer = groupInfo.encode().toArrayBuffer();
|
||||
|
||||
|
@ -93,7 +93,7 @@ describe("GroupBuffer", function() {
|
|||
assert.strictEqual(group.name, "Hackers");
|
||||
assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer);
|
||||
assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']);
|
||||
assert.strictEqual(group.avatar.contentType, "image/jpg");
|
||||
assert.strictEqual(group.avatar.contentType, "image/jpeg");
|
||||
assert.strictEqual(group.avatar.length, 255);
|
||||
assert.strictEqual(group.avatar.data.byteLength, 255);
|
||||
var avatarBytes = new Uint8Array(group.avatar.data);
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
* error: function(message, status, request) {...}
|
||||
* });
|
||||
*
|
||||
* 1. https://github.com/WhisperSystems/WebSocket-Resources
|
||||
* 1. https://github.com/signalapp/WebSocket-Resources
|
||||
*
|
||||
*/
|
||||
|
||||
|
|
107
main.js
|
@ -37,11 +37,17 @@ function getMainWindow() {
|
|||
|
||||
// Tray icon and related objects
|
||||
let tray = null;
|
||||
const startInTray = process.argv.find(arg => arg === '--start-in-tray');
|
||||
const usingTrayIcon = startInTray || process.argv.find(arg => arg === '--use-tray-icon');
|
||||
const startInTray = process.argv.some(arg => arg === '--start-in-tray');
|
||||
const usingTrayIcon = startInTray || process.argv.some(arg => arg === '--use-tray-icon');
|
||||
|
||||
|
||||
const config = require('./app/config');
|
||||
|
||||
const importMode = process.argv.some(arg => arg === '--import') || config.get('import');
|
||||
|
||||
|
||||
const development = config.environment === 'development';
|
||||
|
||||
// Very important to put before the single instance check, since it is based on the
|
||||
// userData directory.
|
||||
const userConfig = require('./app/user_config');
|
||||
|
@ -54,7 +60,7 @@ function showWindow() {
|
|||
// Using focus() instead of show() seems to be important on Windows when our window
|
||||
// has been docked using Aero Snap/Snap Assist. A full .show() call here will cause
|
||||
// the window to reposition:
|
||||
// https://github.com/WhisperSystems/Signal-Desktop/issues/1429
|
||||
// https://github.com/signalapp/Signal-Desktop/issues/1429
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.focus();
|
||||
} else {
|
||||
|
@ -83,7 +89,7 @@ if (!process.mas) {
|
|||
|
||||
if (shouldQuit) {
|
||||
console.log('quitting; we are the second instance');
|
||||
app.quit();
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +125,7 @@ function prepareURL(pathSegments) {
|
|||
appInstance: process.env.NODE_APP_INSTANCE,
|
||||
polyfillNotifications: polyfillNotifications ? true : undefined, // for stringify()
|
||||
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy,
|
||||
importMode: importMode ? true : undefined, // for stringify()
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -157,10 +164,10 @@ function isVisible(window, bounds) {
|
|||
const topClearOfUpperBound = window.y >= boundsY;
|
||||
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
|
||||
|
||||
return rightSideClearOfLeftBound
|
||||
&& leftSideClearOfRightBound
|
||||
&& topClearOfUpperBound
|
||||
&& topClearOfLowerBound;
|
||||
return rightSideClearOfLeftBound &&
|
||||
leftSideClearOfRightBound &&
|
||||
topClearOfUpperBound &&
|
||||
topClearOfLowerBound;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
|
@ -216,6 +223,10 @@ function createWindow() {
|
|||
mainWindow = new BrowserWindow(windowOptions);
|
||||
|
||||
function captureAndSaveWindowStats() {
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size = mainWindow.getSize();
|
||||
const position = mainWindow.getPosition();
|
||||
|
||||
|
@ -277,8 +288,8 @@ function createWindow() {
|
|||
// Emitted when the window is about to be closed.
|
||||
mainWindow.on('close', (e) => {
|
||||
// If the application is terminating, just do the default
|
||||
if (windowState.shouldQuit()
|
||||
|| config.environment === 'test' || config.environment === 'test-lib') {
|
||||
if (windowState.shouldQuit() ||
|
||||
config.environment === 'test' || config.environment === 'test-lib') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -315,11 +326,11 @@ function showDebugLog() {
|
|||
}
|
||||
|
||||
function openReleaseNotes() {
|
||||
shell.openExternal(`https://github.com/WhisperSystems/Signal-Desktop/releases/tag/v${app.getVersion()}`);
|
||||
shell.openExternal(`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`);
|
||||
}
|
||||
|
||||
function openNewBugForm() {
|
||||
shell.openExternal('https://github.com/WhisperSystems/Signal-Desktop/issues/new');
|
||||
shell.openExternal('https://github.com/signalapp/Signal-Desktop/issues/new');
|
||||
}
|
||||
|
||||
function openSupportPage() {
|
||||
|
@ -330,6 +341,24 @@ function openForums() {
|
|||
shell.openExternal('https://whispersystems.discoursehosting.net/');
|
||||
}
|
||||
|
||||
function setupWithImport() {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('set-up-with-import');
|
||||
}
|
||||
}
|
||||
|
||||
function setupAsNewDevice() {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('set-up-as-new-device');
|
||||
}
|
||||
}
|
||||
|
||||
function setupAsStandalone() {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('set-up-as-standalone');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let aboutWindow;
|
||||
function showAbout() {
|
||||
|
@ -373,6 +402,8 @@ function showAbout() {
|
|||
// Some APIs can only be used after this event occurs.
|
||||
let ready = false;
|
||||
app.on('ready', () => {
|
||||
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
||||
/* eslint-disable more/no-then */
|
||||
let loggingSetupError;
|
||||
logging.initialize().catch((error) => {
|
||||
loggingSetupError = error;
|
||||
|
@ -398,22 +429,31 @@ app.on('ready', () => {
|
|||
tray = createTrayIcon(getMainWindow, locale.messages);
|
||||
}
|
||||
|
||||
const options = {
|
||||
showDebugLog,
|
||||
showWindow,
|
||||
showAbout,
|
||||
openReleaseNotes,
|
||||
openNewBugForm,
|
||||
openSupportPage,
|
||||
openForums,
|
||||
};
|
||||
const template = createTemplate(options, locale.messages);
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
setupMenu();
|
||||
});
|
||||
/* eslint-enable more/no-then */
|
||||
});
|
||||
|
||||
function setupMenu(options) {
|
||||
const menuOptions = Object.assign({}, options, {
|
||||
development,
|
||||
showDebugLog,
|
||||
showWindow,
|
||||
showAbout,
|
||||
openReleaseNotes,
|
||||
openNewBugForm,
|
||||
openSupportPage,
|
||||
openForums,
|
||||
setupWithImport,
|
||||
setupAsNewDevice,
|
||||
setupAsStandalone,
|
||||
});
|
||||
const template = createTemplate(menuOptions, locale.messages);
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
|
||||
app.on('before-quit', () => {
|
||||
windowState.markShouldQuit();
|
||||
});
|
||||
|
@ -422,9 +462,9 @@ app.on('before-quit', () => {
|
|||
app.on('window-all-closed', () => {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin'
|
||||
|| config.environment === 'test'
|
||||
|| config.environment === 'test-lib') {
|
||||
if (process.platform !== 'darwin' ||
|
||||
config.environment === 'test' ||
|
||||
config.environment === 'test-lib') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
@ -447,6 +487,17 @@ ipc.on('set-badge-count', (event, count) => {
|
|||
app.setBadgeCount(count);
|
||||
});
|
||||
|
||||
ipc.on('remove-setup-menu-items', () => {
|
||||
setupMenu();
|
||||
});
|
||||
|
||||
ipc.on('add-setup-menu-items', () => {
|
||||
setupMenu({
|
||||
includeSetup: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
ipc.on('draw-attention', () => {
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.bounce();
|
||||
|
|
21
package.json
|
@ -2,17 +2,17 @@
|
|||
"name": "signal-desktop",
|
||||
"productName": "Signal",
|
||||
"description": "Private messaging from your desktop",
|
||||
"repository": "https://github.com/WhisperSystems/Signal-Desktop.git",
|
||||
"version": "1.3.0",
|
||||
"repository": "https://github.com/signalapp/Signal-Desktop.git",
|
||||
"version": "1.5.0-beta.1",
|
||||
"license": "GPL-3.0",
|
||||
"author": {
|
||||
"name": "Open Whisper Systems",
|
||||
"email": "support@whispersystems.org"
|
||||
"email": "support@signal.org"
|
||||
},
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
|
||||
"test": "npm run eslint && npm run test-server && grunt test",
|
||||
"test": "npm run eslint && npm run test-server && grunt test && npm run test-modules",
|
||||
"lint": "grunt jshint",
|
||||
"start": "electron .",
|
||||
"asarl": "asar l release/mac/Signal.app/Contents/Resources/app.asar",
|
||||
|
@ -21,13 +21,15 @@
|
|||
"build": "build --em.environment=$SIGNAL_ENV",
|
||||
"dist": "npm run generate && npm run build",
|
||||
"pack": "npm run generate && npm run build -- --dir",
|
||||
"prepare-build": "node prepare_build.js",
|
||||
"prepare-beta-build": "node prepare_beta_build.js",
|
||||
"prepare-import-build": "node prepare_import_build.js",
|
||||
"pack-prod": "SIGNAL_ENV=production npm run pack",
|
||||
"dist-prod": "SIGNAL_ENV=production npm run dist",
|
||||
"dist-prod-all": "SIGNAL_ENV=production npm run dist -- -mwl",
|
||||
"build-release": "SIGNAL_ENV=production npm run build -- --config.directories.output=release",
|
||||
"build-mas-release": "npm run build-release -- -m --config.mac.target=mas",
|
||||
"build-mas-dev": "npm run build-release -- -m --config.mac.target=mas --config.type=development",
|
||||
"grunt": "grunt",
|
||||
"prep-mac-release": "npm run build-release -- -m --dir",
|
||||
"prep-release": "npm run generate && grunt prep-release && npm run build-release && npm run build-mas-release && grunt test-release",
|
||||
"release-mac": "npm run build-release -- -m --prepackaged release/mac/Signal*.app --publish=always",
|
||||
|
@ -36,10 +38,14 @@
|
|||
"release": "npm run release-mac && npm run release-win && npm run release-lin",
|
||||
"test-server": "mocha --recursive test/server",
|
||||
"test-server-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/server",
|
||||
"test-modules": "mocha --recursive test/modules",
|
||||
"eslint": "eslint .",
|
||||
"open-coverage": "open coverage/lcov-report/index.html"
|
||||
},
|
||||
"dependencies": {
|
||||
"blob-util": "^1.3.0",
|
||||
"blueimp-canvas-to-blob": "^3.14.0",
|
||||
"blueimp-load-image": "^2.18.0",
|
||||
"bunyan": "^1.8.12",
|
||||
"config": "^1.28.1",
|
||||
"electron-config": "^1.0.0",
|
||||
|
@ -62,19 +68,21 @@
|
|||
"rimraf": "^2.6.2",
|
||||
"semver": "^5.4.1",
|
||||
"spellchecker": "^3.4.4",
|
||||
"testcheck": "^1.0.0-rc.2",
|
||||
"websocket": "^1.0.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"asar": "^0.14.0",
|
||||
"bower": "^1.8.2",
|
||||
"chai": "^4.1.2",
|
||||
"electron": "1.7.11",
|
||||
"electron": "1.7.12",
|
||||
"electron-builder": "^19.53.7",
|
||||
"electron-icon-maker": "0.0.3",
|
||||
"electron-publisher-s3": "^19.53.7",
|
||||
"eslint": "^4.14.0",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-more": "^0.3.1",
|
||||
"extract-zip": "^1.6.6",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-cli": "^1.2.0",
|
||||
|
@ -87,6 +95,7 @@
|
|||
"grunt-jscs": "^3.0.1",
|
||||
"grunt-sass": "^2.0.0",
|
||||
"mocha": "^4.1.0",
|
||||
"mocha-testcheck": "^1.0.0-rc.0",
|
||||
"node-sass-import-once": "^1.2.0",
|
||||
"nyc": "^11.4.1",
|
||||
"spectron": "^3.7.2",
|
||||
|
|
32
preload.js
|
@ -42,6 +42,26 @@
|
|||
Whisper.events.trigger('showDebugLog');
|
||||
});
|
||||
|
||||
ipc.on('set-up-with-import', function() {
|
||||
Whisper.events.trigger('setupWithImport');
|
||||
});
|
||||
|
||||
ipc.on('set-up-as-new-device', function() {
|
||||
Whisper.events.trigger('setupAsNewDevice');
|
||||
});
|
||||
|
||||
ipc.on('set-up-as-standalone', function() {
|
||||
Whisper.events.trigger('setupAsStandalone');
|
||||
});
|
||||
|
||||
window.addSetupMenuItems = function() {
|
||||
ipc.send('add-setup-menu-items');
|
||||
}
|
||||
|
||||
window.removeSetupMenuItems = function() {
|
||||
ipc.send('remove-setup-menu-items');
|
||||
}
|
||||
|
||||
// We pull these dependencies in now, from here, because they have Node.js dependencies
|
||||
|
||||
require('./js/logging');
|
||||
|
@ -60,6 +80,8 @@
|
|||
window.nodeSetImmediate(function() {});
|
||||
}, 1000);
|
||||
|
||||
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
||||
window.loadImage = require('blueimp-load-image');
|
||||
window.ProxyAgent = require('proxy-agent');
|
||||
window.EmojiConvertor = require('emoji-js');
|
||||
window.emojiData = require('emoji-datasource');
|
||||
|
@ -70,6 +92,16 @@
|
|||
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
||||
window.nodeNotifier = require('node-notifier');
|
||||
|
||||
const { autoOrientImage } = require('./js/modules/auto_orient_image');
|
||||
window.autoOrientImage = autoOrientImage;
|
||||
|
||||
// ES2015+ modules
|
||||
window.Signal = window.Signal || {};
|
||||
window.Signal.Types = window.Signal.Types || {};
|
||||
window.Signal.Types.Attachment = require('./js/modules/types/attachment');
|
||||
window.Signal.Types.Message = require('./js/modules/types/message');
|
||||
window.Signal.Types.MIME = require('./js/modules/types/mime');
|
||||
|
||||
// We pull this in last, because the native module involved appears to be sensitive to
|
||||
// /tmp mounted as noexec on Linux.
|
||||
require('./js/spell_check');
|
||||
|
|
|
@ -17,7 +17,7 @@ if (!beta.test(version)) {
|
|||
process.exit();
|
||||
}
|
||||
|
||||
console.log('prepare_build: updating package.json for beta build');
|
||||
console.log('prepare_beta_build: updating package.json');
|
||||
|
||||
// -------
|
||||
|
60
prepare_import_build.js
Normal file
|
@ -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, ' '));
|
|
@ -1,4 +1,4 @@
|
|||
package textsecure;
|
||||
package signalservice;
|
||||
|
||||
message ProvisioningUuid {
|
||||
optional string uuid = 1;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package textsecure;
|
||||
// Source: https://github.com/signalapp/libsignal-service-java/blob/4684a49b2ed8f32be619e0d0eea423626b6cb2cb/protobuf/SignalService.proto
|
||||
package signalservice;
|
||||
|
||||
option java_package = "org.whispersystems.textsecure.internal.push";
|
||||
option java_outer_classname = "TextSecureProtos";
|
||||
option java_package = "org.whispersystems.signalservice.internal.push";
|
||||
option java_outer_classname = "SignalServiceProtos";
|
||||
|
||||
message Envelope {
|
||||
enum Type {
|
||||
|
@ -22,40 +23,13 @@ message Envelope {
|
|||
}
|
||||
|
||||
message Content {
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional SyncMessage syncMessage = 2;
|
||||
optional CallMessage callMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional SyncMessage syncMessage = 2;
|
||||
optional CallMessage callMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional ReceiptMessage receiptMessage = 5;
|
||||
}
|
||||
|
||||
message ReceiptMessage {
|
||||
enum Type {
|
||||
DELIVERY = 0;
|
||||
READ = 1;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
repeated uint64 timestamps = 2;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
optional bytes padding = 1;
|
||||
}
|
||||
|
||||
message Verified {
|
||||
enum State {
|
||||
DEFAULT = 0;
|
||||
VERIFIED = 1;
|
||||
UNVERIFIED = 2;
|
||||
}
|
||||
|
||||
optional string destination = 1;
|
||||
optional bytes identityKey = 2;
|
||||
optional State state = 3;
|
||||
optional bytes nullMessage = 4;
|
||||
}
|
||||
|
||||
message CallMessage {
|
||||
message Offer {
|
||||
optional uint64 id = 1;
|
||||
|
@ -92,7 +66,7 @@ message CallMessage {
|
|||
|
||||
message DataMessage {
|
||||
enum Flags {
|
||||
END_SESSION = 1;
|
||||
END_SESSION = 1;
|
||||
EXPIRATION_TIMER_UPDATE = 2;
|
||||
PROFILE_KEY_UPDATE = 4;
|
||||
}
|
||||
|
@ -103,6 +77,34 @@ message DataMessage {
|
|||
optional uint32 flags = 4;
|
||||
optional uint32 expireTimer = 5;
|
||||
optional bytes profileKey = 6;
|
||||
optional uint64 timestamp = 7;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
optional bytes padding = 1;
|
||||
}
|
||||
|
||||
message ReceiptMessage {
|
||||
enum Type {
|
||||
DELIVERY = 0;
|
||||
READ = 1;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
repeated uint64 timestamp = 2;
|
||||
}
|
||||
|
||||
message Verified {
|
||||
enum State {
|
||||
DEFAULT = 0;
|
||||
VERIFIED = 1;
|
||||
UNVERIFIED = 2;
|
||||
}
|
||||
|
||||
optional string destination = 1;
|
||||
optional bytes identityKey = 2;
|
||||
optional State state = 3;
|
||||
optional bytes nullMessage = 4;
|
||||
}
|
||||
|
||||
message SyncMessage {
|
||||
|
@ -115,7 +117,7 @@ message SyncMessage {
|
|||
|
||||
message Contacts {
|
||||
optional AttachmentPointer blob = 1;
|
||||
optional bool isComplete = 2 [default = false];
|
||||
optional bool complete = 2 [default = false];
|
||||
}
|
||||
|
||||
message Groups {
|
||||
|
@ -143,19 +145,19 @@ message SyncMessage {
|
|||
optional uint64 timestamp = 2;
|
||||
}
|
||||
|
||||
message Settings {
|
||||
message Configuration {
|
||||
optional bool readReceipts = 1;
|
||||
}
|
||||
|
||||
optional Sent sent = 1;
|
||||
optional Contacts contacts = 2;
|
||||
optional Groups groups = 3;
|
||||
optional Request request = 4;
|
||||
repeated Read read = 5;
|
||||
optional Blocked blocked = 6;
|
||||
optional Verified verified = 7;
|
||||
optional bytes padding = 8;
|
||||
optional Settings settings = 9;
|
||||
optional Sent sent = 1;
|
||||
optional Contacts contacts = 2;
|
||||
optional Groups groups = 3;
|
||||
optional Request request = 4;
|
||||
repeated Read read = 5;
|
||||
optional Blocked blocked = 6;
|
||||
optional Verified verified = 7;
|
||||
optional Configuration configuration = 9;
|
||||
optional bytes padding = 8;
|
||||
}
|
||||
|
||||
message AttachmentPointer {
|
||||
|
@ -171,6 +173,8 @@ message AttachmentPointer {
|
|||
optional bytes digest = 6;
|
||||
optional string fileName = 7;
|
||||
optional uint32 flags = 8;
|
||||
optional uint32 width = 9;
|
||||
optional uint32 height = 10;
|
||||
}
|
||||
|
||||
message GroupContext {
|
||||
|
@ -194,12 +198,14 @@ message ContactDetails {
|
|||
optional uint32 length = 2;
|
||||
}
|
||||
|
||||
optional string number = 1;
|
||||
optional string name = 2;
|
||||
optional Avatar avatar = 3;
|
||||
optional string color = 4;
|
||||
optional Verified verified = 5;
|
||||
optional bytes profileKey = 6;
|
||||
optional string number = 1;
|
||||
optional string name = 2;
|
||||
optional Avatar avatar = 3;
|
||||
optional string color = 4;
|
||||
optional Verified verified = 5;
|
||||
optional bytes profileKey = 6;
|
||||
optional bool blocked = 7;
|
||||
optional uint32 expireTimer = 8;
|
||||
}
|
||||
|
||||
message GroupDetails {
|
||||
|
@ -208,9 +214,10 @@ message GroupDetails {
|
|||
optional uint32 length = 2;
|
||||
}
|
||||
|
||||
optional bytes id = 1;
|
||||
optional string name = 2;
|
||||
repeated string members = 3;
|
||||
optional Avatar avatar = 4;
|
||||
optional bool active = 5 [default = true];
|
||||
optional bytes id = 1;
|
||||
optional string name = 2;
|
||||
repeated string members = 3;
|
||||
optional Avatar avatar = 4;
|
||||
optional bool active = 5 [default = true];
|
||||
optional uint32 expireTimer = 6;
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package textsecure;
|
||||
package signalservice;
|
||||
|
||||
option java_package = "org.whispersystems.websocket.messages.protobuf";
|
||||
|
||||
|
@ -42,4 +42,4 @@ message WebSocketMessage {
|
|||
optional Type type = 1;
|
||||
optional WebSocketRequestMessage request = 2;
|
||||
optional WebSocketResponseMessage response = 3;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package textsecure;
|
||||
package signalservice;
|
||||
|
||||
option java_package = "org.whispersystems.libsignal.protocol";
|
||||
option java_outer_classname = "WhisperProtos";
|
||||
|
|
|
@ -218,7 +218,7 @@ button.hamburger {
|
|||
}
|
||||
|
||||
.dropoff {
|
||||
outline: solid 1px #2090ea;
|
||||
outline: solid 1px $blue;
|
||||
}
|
||||
|
||||
$avatar-size: 44px;
|
||||
|
@ -609,6 +609,281 @@ input[type=text], input[type=search], textarea {
|
|||
}
|
||||
}
|
||||
|
||||
.full-screen-flow {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
font-family: roboto-light;
|
||||
|
||||
color: black;
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgb(238,238,238) 0%, // (1 - 0.41) * 255 + 0.41 * 213
|
||||
rgb(243,243,243) 12%, // (1 - 0.19) * 255 + 0.19 * 191
|
||||
rgb(255,255,255) 27%,
|
||||
rgb(255,255,255) 60%,
|
||||
rgb(249,249,249) 85%, // (1 - 0.19) * 255 + 0.19 * 222
|
||||
rgb(213,213,213) 100% // (1 - 0.27) * 255 + 0.27 * 98
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
font-size: 10pt;
|
||||
input {
|
||||
margin-top: 1em;
|
||||
font-size: 12pt;
|
||||
font-family: roboto-light;
|
||||
border: 2px solid $blue;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
@media (min-height: 750px) and (min-width: 700px) {
|
||||
font-size: 14pt;
|
||||
|
||||
input {
|
||||
font-size: 16pt;
|
||||
}
|
||||
}
|
||||
|
||||
#qr {
|
||||
display: inline-block;
|
||||
|
||||
&.ready {
|
||||
border: 5px solid $blue;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
img {
|
||||
height: 20em;
|
||||
border: 5px solid white;
|
||||
}
|
||||
|
||||
@media (max-height: 475px) {
|
||||
img {
|
||||
width: 8em;
|
||||
height: 8em;
|
||||
}
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 3px solid $blue;
|
||||
border-radius: 50%;
|
||||
float: left;
|
||||
margin: 0 6px;
|
||||
transform: scale(0);
|
||||
|
||||
animation: loading 1500ms ease infinite 0ms;
|
||||
&:nth-child(2) {
|
||||
animation: loading 1500ms ease infinite 333ms;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation: loading 1500ms ease infinite 666ms;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.os-icon {
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin: 0.5em;
|
||||
|
||||
&.apple {
|
||||
@include color-svg('../images/apple.svg', black);
|
||||
}
|
||||
&.android {
|
||||
@include color-svg('../images/android.svg', black);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: normal;
|
||||
margin-bottom: 1.5em;
|
||||
|
||||
font-size: 20pt;
|
||||
|
||||
@media (min-height: 750px) and (min-width: 700px) {
|
||||
font-size: 28pt;
|
||||
}
|
||||
}
|
||||
|
||||
.body-text {
|
||||
max-width: 22em;
|
||||
text-align: left;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.body-text-wide {
|
||||
max-width: 30em;
|
||||
text-align: left;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.step {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 70px 0 50px;
|
||||
}
|
||||
.step-body {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 35em;
|
||||
}
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
margin: 1em;
|
||||
display: none;
|
||||
|
||||
@media (min-height: 550px) {
|
||||
display: inline-block;
|
||||
height: 10em;
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
display: none;
|
||||
margin: 1em;
|
||||
|
||||
// 640px by 338px is the smallest the window can go
|
||||
@media (min-height: 550px) {
|
||||
display: inline-block;
|
||||
height: 10em;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
// generic
|
||||
&.check-circle-outline {
|
||||
@include color-svg('../images/check-circle-outline.svg', #DEDEDE);
|
||||
}
|
||||
&.alert-outline {
|
||||
@include color-svg('../images/alert-outline.svg', #DEDEDE);
|
||||
}
|
||||
|
||||
// import and export
|
||||
&.folder-outline {
|
||||
@include color-svg('../images/folder-outline.svg', #DEDEDE);
|
||||
}
|
||||
&.import {
|
||||
@include color-svg('../images/import.svg', #DEDEDE);
|
||||
}
|
||||
&.export {
|
||||
@include color-svg('../images/export.svg', #DEDEDE);
|
||||
}
|
||||
|
||||
// registration process
|
||||
&.lead-pencil {
|
||||
@include color-svg('../images/lead-pencil.svg', #DEDEDE);
|
||||
}
|
||||
&.sync {
|
||||
@include color-svg('../images/sync.svg', #DEDEDE);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
min-width: 300px;
|
||||
padding: 0.75em;
|
||||
margin-top: 1em;
|
||||
color: white;
|
||||
background: $blue;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
|
||||
font-size: 12pt;
|
||||
|
||||
@media (min-height: 750px) and (min-width: 700px) {
|
||||
font-size: 20pt;
|
||||
}
|
||||
}
|
||||
a.link {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
margin: 0.5em;
|
||||
color: #2090ea;
|
||||
}
|
||||
|
||||
.progress {
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
|
||||
.bar-container {
|
||||
height: 1em;
|
||||
margin: 1em;
|
||||
background-color: $grey_l;
|
||||
}
|
||||
.bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $blue_l;
|
||||
transition: width 0.25s;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 100%;
|
||||
bottom: 50px;
|
||||
margin-top: auto;
|
||||
padding-bottom: 2em;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
|
||||
.instructions {
|
||||
text-align: left;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 2em;
|
||||
margin-top: 2em;
|
||||
max-width: 30em;
|
||||
}
|
||||
.instructions:after {
|
||||
clear: both;
|
||||
}
|
||||
.android {
|
||||
float: left;
|
||||
}
|
||||
.apple {
|
||||
float: right;
|
||||
}
|
||||
.label {
|
||||
float: left;
|
||||
}
|
||||
.body {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//yellow border fix
|
||||
.inbox:focus {
|
||||
outline: none;
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.device-name-settings {
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.syncSettings {
|
||||
button {
|
||||
float: right;
|
||||
|
|
|
@ -6,335 +6,6 @@
|
|||
background: url("../images/flags.png");
|
||||
}
|
||||
|
||||
.install {
|
||||
height: 100%;
|
||||
background: #2090ea;
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
overflow: auto;
|
||||
|
||||
input, button, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 70px 0 50px;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.step {
|
||||
height: 100%;
|
||||
}
|
||||
.inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.step-body {
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
#signal-computer,
|
||||
#signal-phone {
|
||||
max-width: 50%;
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 35em;
|
||||
margin: 1em auto;
|
||||
padding: 0 1em;
|
||||
line-height: 1.5em;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
&, &:visited, &:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
min-width: 300px;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
background: white;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 100%;
|
||||
bottom: 50px;
|
||||
margin-top: auto;
|
||||
padding: 20px;
|
||||
|
||||
.dot-container {
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: white;
|
||||
border: solid 5px $blue;
|
||||
|
||||
&.selected {
|
||||
background: $blue_l;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.install-choice .nav {
|
||||
top: 20px;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.link {
|
||||
&:hover, &:focus {
|
||||
background: rgba(255,255,255,0.3);
|
||||
outline: none;
|
||||
}
|
||||
&, &:visited, &:hover {
|
||||
padding: 0 3px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border-bottom: dashed 2px white;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 650px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30pt;
|
||||
font-weight: normal;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h3.step {
|
||||
margin-top: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.help {
|
||||
border-top: 2px solid $grey_l;
|
||||
padding: 1.5em 0.1em;
|
||||
}
|
||||
|
||||
.install {
|
||||
display: inline-block;
|
||||
margin-top: 90px;
|
||||
}
|
||||
|
||||
#qr {
|
||||
display: inline-block;
|
||||
min-height: 266px;
|
||||
img {
|
||||
border: 5px solid white;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#device-name {
|
||||
border: none;
|
||||
border-bottom: 1px solid white;
|
||||
padding: 8px;
|
||||
background: transparent;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
&::selection, a::selection {
|
||||
color: $grey_d;
|
||||
background: white;
|
||||
}
|
||||
|
||||
&::-moz-selection, a::-moz-selection {
|
||||
color: $grey_d;
|
||||
background: white;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#verifyCode,
|
||||
#code,
|
||||
#number {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#request-voice,
|
||||
#request-sms {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#request-sms {
|
||||
width: 57%;
|
||||
float: right;
|
||||
}
|
||||
#request-voice {
|
||||
width: 40%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.number-container {
|
||||
position: relative;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.number-container .intl-tel-input,
|
||||
.number-container .number {
|
||||
width: 100%;
|
||||
}
|
||||
.number-container::after {
|
||||
visibility: hidden;
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
border-radius: 1.5em;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
line-height: 1.5em;
|
||||
color: #ffffff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
margin: 3px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.number-container.valid::after {
|
||||
visibility: visible;
|
||||
content: '✓';
|
||||
background-color: #0f9d58;
|
||||
color: #ffffff;
|
||||
}
|
||||
.number-container.invalid::after {
|
||||
visibility: visible;
|
||||
content: '!';
|
||||
background-color: #f44336;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#error {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
#error { background-color: #f44336; }
|
||||
#error:before {
|
||||
content: '\26a0';
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
.narrow {
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
width: 275px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
ul.country-list {
|
||||
min-width: 197px !important;
|
||||
}
|
||||
|
||||
.confirmation-dialog, .progress-dialog {
|
||||
padding: 1em;
|
||||
text-align: left;
|
||||
}
|
||||
.number { text-align: center; }
|
||||
.confirmation-dialog {
|
||||
button {
|
||||
float: right;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.progress-dialog {
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
|
||||
.status { padding: 1em; }
|
||||
|
||||
.bar-container {
|
||||
height: 1em;
|
||||
background-color: $grey_l;
|
||||
border: solid 1px white;
|
||||
}
|
||||
.bar {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background-color: $blue_l;
|
||||
transition: width 0.25s;
|
||||
|
||||
&.active {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.1);
|
||||
top: 0;
|
||||
padding-top: 10em;
|
||||
text-align: center;
|
||||
|
||||
.modal-main {
|
||||
display: inline-block;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
border: solid 2px $blue;
|
||||
background: white;
|
||||
margin: 10% auto;
|
||||
box-shadow: 0 0 5px 3px rgba(darken($blue, 30%), 0.2);
|
||||
|
||||
h4 {
|
||||
background-color: $blue;
|
||||
color: white;
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.intl-tel-input .country-list {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
@ -450,7 +450,7 @@
|
|||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='link_to_support'>
|
||||
<a href='http://support.whispersystems.org/hc/articles/213134107' target='_blank'>
|
||||
<a href='http://support.signal.org/hc/articles/213134107' target='_blank'>
|
||||
{{ learnMore }}
|
||||
</a>
|
||||
</script>
|
||||
|
@ -476,7 +476,7 @@
|
|||
</div>
|
||||
<p>
|
||||
<a class='report-link' target='_blank'
|
||||
href='https://github.com/WhisperSystems/Signal-Desktop/issues/new/'>
|
||||
href='https://github.com/signalapp/Signal-Desktop/issues/new/'>
|
||||
{{ reportIssue }}
|
||||
</a>
|
||||
</p>
|
||||
|
|
6
test/modules/.eslintrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"globals": {
|
||||
"check": true,
|
||||
"gen": true
|
||||
}
|
||||
}
|
246
test/modules/types/attachment_test.js
Normal 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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
30
test/modules/types/mime_test.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const { assert } = require('chai');
|
||||
|
||||
const MIME = require('../../../js/modules/types/mime');
|
||||
|
||||
|
||||
describe('MIME', () => {
|
||||
describe('isJPEG', () => {
|
||||
it('should return true for `image/jpeg`', () => {
|
||||
assert.isTrue(MIME.isJPEG('image/jpeg'));
|
||||
});
|
||||
|
||||
[
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'image/jpg', // invalid MIME type: https://stackoverflow.com/a/37266399/125305
|
||||
'image/gif',
|
||||
'image/tiff',
|
||||
'application/json',
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
undefined,
|
||||
]
|
||||
.forEach((value) => {
|
||||
it(`should return false for \`${value}\``, () => {
|
||||
assert.isFalse(MIME.isJPEG(value));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,3 +1,6 @@
|
|||
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
|
|
@ -2,18 +2,33 @@
|
|||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
describe('LastSeenIndicatorView', function() {
|
||||
// TODO: in electron branch, where we have access to real i18n, test rendered HTML
|
||||
|
||||
it('renders provided count', function() {
|
||||
var view = new Whisper.LastSeenIndicatorView({count: 10});
|
||||
assert.equal(view.count, 10);
|
||||
|
||||
view.render();
|
||||
assert.match(view.$el.html(), /10 Unread Messages/);
|
||||
});
|
||||
|
||||
it('renders count of 1', function() {
|
||||
var view = new Whisper.LastSeenIndicatorView({count: 1});
|
||||
assert.equal(view.count, 1);
|
||||
|
||||
view.render();
|
||||
assert.match(view.$el.html(), /1 Unread Message/);
|
||||
});
|
||||
|
||||
it('increments count', function() {
|
||||
var view = new Whisper.LastSeenIndicatorView({count: 4});
|
||||
|
||||
assert.equal(view.count, 4);
|
||||
view.render();
|
||||
assert.match(view.$el.html(), /4 Unread Messages/);
|
||||
|
||||
view.increment(3);
|
||||
assert.equal(view.count, 7);
|
||||
view.render();
|
||||
assert.match(view.$el.html(), /7 Unread Messages/);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -2,13 +2,11 @@
|
|||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
describe('ScrollDownButtonView', function() {
|
||||
// TODO: in electron branch, where we have access to real i18n, uncomment assertions against real strings
|
||||
|
||||
it('renders with count = 0', function() {
|
||||
var view = new Whisper.ScrollDownButtonView();
|
||||
view.render();
|
||||
assert.equal(view.count, 0);
|
||||
// assert.match(view.$el.html(), /Scroll to bottom/);
|
||||
assert.match(view.$el.html(), /Scroll to bottom/);
|
||||
});
|
||||
|
||||
it('renders with count = 1', function() {
|
||||
|
@ -16,7 +14,7 @@ describe('ScrollDownButtonView', function() {
|
|||
view.render();
|
||||
assert.equal(view.count, 1);
|
||||
assert.match(view.$el.html(), /new-messages/);
|
||||
// assert.match(view.$el.html(), /New message below/);
|
||||
assert.match(view.$el.html(), /New message below/);
|
||||
});
|
||||
|
||||
it('renders with count = 2', function() {
|
||||
|
@ -25,7 +23,7 @@ describe('ScrollDownButtonView', function() {
|
|||
assert.equal(view.count, 2);
|
||||
|
||||
assert.match(view.$el.html(), /new-messages/);
|
||||
// assert.match(view.$el.html(), /New messages below/);
|
||||
assert.match(view.$el.html(), /New messages below/);
|
||||
});
|
||||
|
||||
it('increments count and re-renders', function() {
|
||||
|
|
55
yarn.lock
|
@ -437,6 +437,17 @@ bl@^1.0.0:
|
|||
dependencies:
|
||||
readable-stream "^2.0.5"
|
||||
|
||||
blob-util@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-1.3.0.tgz#dbb4e8caffd50b5720d347e1169b6369ba34fe95"
|
||||
dependencies:
|
||||
blob "0.0.4"
|
||||
native-or-lie "1.0.2"
|
||||
|
||||
blob@0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
|
||||
|
||||
block-stream@*:
|
||||
version "0.0.9"
|
||||
resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
|
||||
|
@ -457,6 +468,14 @@ bluebird@^3.5.1:
|
|||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
||||
|
||||
blueimp-canvas-to-blob@^3.14.0:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.14.0.tgz#ea075ffbfb1436607b0c75e951fb1ceb3ca0288e"
|
||||
|
||||
blueimp-load-image@^2.18.0:
|
||||
version "2.18.0"
|
||||
resolved "https://registry.yarnpkg.com/blueimp-load-image/-/blueimp-load-image-2.18.0.tgz#03b93687eb382a7136cfbcbd4f0e936b6763fc0e"
|
||||
|
||||
bmp-js@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.1.tgz#5ad0147099d13a9f38aa7b99af1d6e78666ed37f"
|
||||
|
@ -1516,9 +1535,9 @@ electron-updater@^2.19.0:
|
|||
semver "^5.4.1"
|
||||
source-map-support "^0.5.0"
|
||||
|
||||
electron@1.7.11:
|
||||
version "1.7.11"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.11.tgz#993b6aa79e0e79a7cfcc369f4c813fbd9a0b08d9"
|
||||
electron@1.7.12:
|
||||
version "1.7.12"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.12.tgz#dcc61a2c1b0c3df25f68b3425379a01abd01190e"
|
||||
dependencies:
|
||||
"@types/node" "^7.0.18"
|
||||
electron-download "^3.0.1"
|
||||
|
@ -1642,6 +1661,10 @@ eslint-plugin-import@^2.8.0:
|
|||
minimatch "^3.0.3"
|
||||
read-pkg-up "^2.0.0"
|
||||
|
||||
eslint-plugin-more@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-more/-/eslint-plugin-more-0.3.1.tgz#ff688fb3fa8f153c8bfd5d70c15a68dc222a1b31"
|
||||
|
||||
eslint-restricted-globals@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
|
||||
|
@ -2606,6 +2629,10 @@ ignore@^3.3.3:
|
|||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
|
||||
import-lazy@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
|
||||
|
@ -3150,6 +3177,12 @@ levn@^0.3.0, levn@~0.3.0:
|
|||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
lie@*:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.2.0.tgz#4f13f2f8bbb027d383db338c43043545791aa8dc"
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
livereload-js@^2.2.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2"
|
||||
|
@ -3439,6 +3472,12 @@ mksnapshot@^0.3.0:
|
|||
fs-extra "0.26.7"
|
||||
request "^2.79.0"
|
||||
|
||||
mocha-testcheck@^1.0.0-rc.0:
|
||||
version "1.0.0-rc.0"
|
||||
resolved "https://registry.yarnpkg.com/mocha-testcheck/-/mocha-testcheck-1.0.0-rc.0.tgz#05e50203043be1537aef2a87dd96ccd447702773"
|
||||
dependencies:
|
||||
testcheck "^1.0.0-rc"
|
||||
|
||||
mocha@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794"
|
||||
|
@ -3490,6 +3529,12 @@ nan@^2.0.0, nan@^2.3.2, nan@^2.3.3:
|
|||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
|
||||
|
||||
native-or-lie@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/native-or-lie/-/native-or-lie-1.0.2.tgz#c870ee0ba0bf0ff11350595d216cfea68a6d8086"
|
||||
dependencies:
|
||||
lie "*"
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
|
@ -4947,6 +4992,10 @@ test-exclude@^4.1.1:
|
|||
read-pkg-up "^1.0.1"
|
||||
require-main-filename "^1.0.1"
|
||||
|
||||
testcheck@^1.0.0-rc, testcheck@^1.0.0-rc.2:
|
||||
version "1.0.0-rc.2"
|
||||
resolved "https://registry.yarnpkg.com/testcheck/-/testcheck-1.0.0-rc.2.tgz#11356a25b84575efe0b0857451e85b5fa74ee4e4"
|
||||
|
||||
text-table@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
|
|