New design for import/install, 'light' import (#2053)

- A new design for the import flow. It features:
  - Icons at the top of every screen
  - Gray background, blue buttons, thinner text
  - Simpler copy
- A new design for the install flow. It features:
  - Immediate entry into the QR code screen
  - Animated dots to show that we're loading the QR code from the server
  - Fewer screens: 1) QR 2) device name 3) sync-in-progress
- When not set up, the app opens directly into the install screen, which has been streamlined. The `--import` command-line argument will cause the app to open directly into the import flow.
- Support for two different flavors of builds - the normal build will open into the standard registration flow, and the import flavor will be exactly the same except during setup it will open directly into the import flow.
- A new design for the (dev-only) standalone registration view
- When these install sequences are active, the OS File menu has entries to allow you to switch the method of setup you'd like to use. These go away as soon as the first step is taken in any of these flows.
- The device name (chosen on initial setup) is now shown in the settings panel
- At the end of a light import, we hand off to the normal device link screen, starting at the QR code. On a full import, we remove the sensitive encryption information in the export to prevent conflicts on multiple imports.
- `Whisper.Backup.exportToDirectory()` takes an options object so you can tell it to do a light export.
- `Whisper.Backup.importFromDirectory()` takes an options object so you can force it to load only the light components found on disk. It also returns an object so you can tell whether a given import was a full import or light import.
- On start of import, we build a list of all the ids present in the messages, conversations, and groups stores in IndexedDB. This can take some time if a lot of data is in the database already, but it makes the subsequent deduplicated import very fast.
- Disappearing messages are now excluded when exporting
- Remove some TODOs in the tests
This commit is contained in:
Scott Nonnenberg 2018-02-22 10:40:32 -08:00 committed by GitHub
parent a1ac810343
commit 426dab85a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1228 additions and 835 deletions

View file

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

View file

@ -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.signal.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.signal.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."
},

View file

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

View file

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

View file

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

View file

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

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

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

After

Width:  |  Height:  |  Size: 357 B

1
images/android.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 955 B

1
images/apple.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 824 B

View file

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

After

Width:  |  Height:  |  Size: 499 B

View file

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

After

Width:  |  Height:  |  Size: 401 B

1
images/import.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 421 B

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

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

After

Width:  |  Height:  |  Size: 584 B

1
images/sync.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 521 B

View file

@ -107,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');
@ -140,8 +161,10 @@
appView.openInbox({
initialLoadComplete: initialLoadComplete
});
} else if (window.config.importMode) {
appView.openImporter();
} else {
appView.openInstallChoice();
appView.openInstaller();
}
Whisper.events.on('showDebugLog', function() {
@ -158,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) {

View file

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

View file

@ -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,29 @@
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 = {
startStep: Whisper.InstallView.Steps.SCAN_QR_CODE,
};
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) {
window.addSetupMenuItems();
this.resetViews();
var installView = this.installView = new Whisper.InstallView(options);
this.openView(this.installView);
},
closeInstaller: function() {
@ -88,11 +78,23 @@
},
openStandalone: function() {
if (window.config.environment !== 'production') {
this.closeInstaller();
this.installView = new Whisper.StandaloneRegistrationView();
this.openView(this.installView);
window.addSetupMenuItems();
this.resetViews();
this.standaloneView = new Whisper.StandaloneRegistrationView();
this.openView(this.standaloneView);
}
},
closeStandalone: function() {
if (this.standaloneView) {
this.standaloneView.remove();
this.standaloneView = null;
}
},
resetViews: function() {
this.closeInstaller();
this.closeImporter();
this.closeStandalone();
},
openInbox: function(options) {
options = options || {};
// The inbox can be created before the 'empty' event fires or afterwards. If

View file

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

View file

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

View file

@ -14,145 +14,159 @@
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/signalapp';
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();
if (Whisper.Registration.everDone() || options.startStep) {
this.selectStep(options.startStep || Steps.SCAN_QR_CODE);
}
},
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);
}.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();
}
});
Whisper.InstallView.Steps = Steps;
})();

View file

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

View file

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

74
main.js
View file

@ -37,11 +37,17 @@ function getMainWindow() {
// Tray icon and related objects
let tray = null;
const startInTray = process.argv.find(arg => arg === '--start-in-tray');
const usingTrayIcon = startInTray || process.argv.find(arg => arg === '--use-tray-icon');
const startInTray = process.argv.some(arg => arg === '--start-in-tray');
const usingTrayIcon = startInTray || process.argv.some(arg => arg === '--use-tray-icon');
const config = require('./app/config');
const importMode = process.argv.some(arg => arg === '--import') || config.get('import');
const development = config.environment === 'development';
// Very important to put before the single instance check, since it is based on the
// userData directory.
const userConfig = require('./app/user_config');
@ -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()
},
});
}
@ -334,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() {
@ -404,23 +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();
});
@ -454,6 +487,17 @@ ipc.on('set-badge-count', (event, count) => {
app.setBadgeCount(count);
});
ipc.on('remove-setup-menu-items', () => {
setupMenu();
});
ipc.on('add-setup-menu-items', () => {
setupMenu({
includeSetup: true,
});
});
ipc.on('draw-attention', () => {
if (process.platform === 'darwin') {
app.dock.bounce();

View file

@ -21,7 +21,8 @@
"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",

View file

@ -42,6 +42,26 @@
Whisper.events.trigger('showDebugLog');
});
ipc.on('set-up-with-import', function() {
Whisper.events.trigger('setupWithImport');
});
ipc.on('set-up-as-new-device', function() {
Whisper.events.trigger('setupAsNewDevice');
});
ipc.on('set-up-as-standalone', function() {
Whisper.events.trigger('setupAsStandalone');
});
window.addSetupMenuItems = function() {
ipc.send('add-setup-menu-items');
}
window.removeSetupMenuItems = function() {
ipc.send('remove-setup-menu-items');
}
// We pull these dependencies in now, from here, because they have Node.js dependencies
require('./js/logging');

View file

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

60
prepare_import_build.js Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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