Updated export flow visuals, new upgrade banner, light export (#2067)

* Backup: Exclude all disappearing messages from export

* Reset overall statistics on start of backup

* Updated visual design for the export process

* On any export after first, don't include encryption info

* Wire up the upgrade banner with dismiss button

Unlike the previous implementation of this banner, it does not go away
when the user clicks on a conversation.

* Small tweak to the export error page: text width, error->isError

* Responding to PR feedback: some refactors, typo fix, dev server

- backupToDirectory -> exportToDirectory
- backup.js attachments -> numAttachments
- migration_view.js comment typo fix
- background.js revert back to the staging server url
- database.js remove dev-only commented-out migration

* Incremental rollout for upgrade banner

* Fix timing, address other PR feedback

* exportingToDirectory -> exportToDirectory

* inboxView: call proper method to show export process

* Remove unnecessary cancel() if first export was never finished

Since we do want to keep track of whether previous exports were even
attempted, this works against our 'light export/import' strategy. It's
also unnecessary, because the user will start at the first step of the
process.

* Log if we get the same value back from the s3 trigger file

* Migration.cancel() - don't reset 'everAttempted' flag

This would prevent us from doing the light export that second time
through.
This commit is contained in:
Scott Nonnenberg
2018-02-27 14:10:57 -08:00
committed by GitHub
parent 636fae1857
commit a02612cd11
19 changed files with 1187 additions and 272 deletions

View File

@@ -1,15 +1,7 @@
{
"cancelMigration": {
"message": "Cancel migration",
"description": "Shown on 'export complete' screen, allowing user to use Chrome App again"
},
"continueMigration": {
"message": "Continue migration",
"description": "Shown on 'cancel migration' confirmation dialog, dismissing the dialog without restoring Chrome App use',"
},
"cancelWarning": {
"message": "Are you sure? If you import the data you've already exported, it will conflict with this installation. Please consider deleting all exported data if you continue with cancellation.",
"description": "Shown on a confirmation dialog presented before the user cancels out of the migration process"
"close": {
"message": "Close",
"description": "Shown on a button to dismiss a dialog box"
},
"loading": {
"message": "Loading...",
@@ -19,63 +11,101 @@
"message": "Install is complete",
"description": "Button to click when user has installed the new Signal Desktop"
},
"migrationWarning": {
"message": "The Signal Desktop Chrome app has been deprecated. Would you like to migrate to the new Signal Desktop now?",
"description": "Warning notification that this version of the app has been deprecated and the user must migrate"
"upgradeBanner": {
"message": "The next generation of Signal Desktop is here.",
"description": "Shown at the top of the application prompting the user to upgrade to the new, standalone version of the app."
},
"upgradeNow": {
"message": "Upgrade now",
"description": "The button shown in the upgrade banner, starting the upgrade process."
},
"migrateToStandalone": {
"message": "Migrate to standalone",
"description": "Menu option to begin migrating this client to Electron"
},
"migrateInstallStep": {
"message": "The first step is to install the new Signal Desktop.",
"description": "The first step in the export process; installing the standalone desktop app, to ensure it is available for that platform."
},
"exportInstructions": {
"message": "Now, 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"
"saveDataPrompt": {
"message": "Your messages, contacts, groups, and other information can be seamlessly transferred to the new Signal Desktop. Select the folder where your exported Signal data should be saved.",
"description": "Explanation of what will happen when the user presses the 'choose folder' button during the upgrade process"
},
"migrate": {
"message": "Migrate",
"description": "Button label to begin migrating this client to Electron"
"chooseFolder": {
"message": "Choose folder",
"description": "Button which pops up a directory selection dialog, and starts saving data to the selected folder"
},
"migrateToStandalone": {
"message": "Migrate to standalone",
"description": "Menu option to begin migrating this client to Electron"
"startExportHeader": {
"message": "Upgrade to the new Signal Desktop",
"description": "Header shown when starting the export and upgrade process"
},
"export": {
"message": "Choose directory",
"description": "Button to allow the user to export all data from app as part of migration process"
"startExportIntro": {
"message": "Signal Desktop is now available as a standalone application that features faster performance, expanded emoji support (with a brand-new emoji selector), enhanced notifications, and many other improvements. This process will prepare your Signal data for a smooth transition to upgrade.",
"description": "Description of the why and the what of the export and upgrade process"
},
"exportAgain": {
"message": "Export again",
"description": "If user has already exported once, this button allows user to do it again if needed"
"imReady": {
"message": "I'm ready",
"description": "Button to kick off the export and upgrade process"
},
"installHeader": {
"message": "Select your Operating System",
"description": "Header text for the install step of the export and upgrade flow"
},
"installIntro": {
"message": "Download and install the new Signal Desktop for your chosen OS.",
"description": "Introduction text for the install step of the export and upgrade flow"
},
"macOS": {
"message": "macOS",
"description": "The name of Apple's desktop operating system"
},
"windows": {
"message": "Windows",
"description": "The name of Microsoft's desktop operating system"
},
"debianLinux": {
"message": "Debian-based Linux",
"description": "A succinct name for the flavors of linux based on Debian"
},
"installed": {
"message": "It's installed",
"description": "The button indicating that the user is done with the installation step"
},
"saveHeader": {
"message": "Save your data",
"description": "The header for the choose folder step of the export and upgrade flow"
},
"completeHeader": {
"message": "Success!",
"description": "The header for the finish step of the export and upgrade flow"
},
"completeIntro": {
"message": "Your Signal data was successfully saved here:",
"description": "A label for the location of the files we put on disk during the export"
},
"completeNextSteps": {
"message": "You are ready to make your move. Launch the new Signal Desktop and follow the instructions to complete the migration process.",
"description": "A summary of the next steps to finish the export and upgrade process"
},
"completeSignoff": {
"message": "We hope that you enjoy the new features. We're just getting started. Thanks for using Signal Desktop.",
"description": "A warm signoff now that the export and upgrade process is complete"
},
"exportErrorHeader": {
"message": "Something went wrong!",
"description": "Header shown at the top of the error screen if something goes wrong during data export"
},
"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!",
"message": "Unfortunately, something went wrong during the export. First, check that you have enough space where you saved the file. 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"
},
"confirmMigration": {
"message": "Start migration process? You will not be able to send or receive Signal messages from this application while the migration is in progress.",
"description": "Confirmation dialogue when beginning migration"
"chooseFolderAndTryAgain": {
"message": "Choose folder and try again",
"description": "Button shown on the export error screen allowing user to try again"
},
"migrationDisconnecting": {
"message": "Disconnecting...",
"description": "Displayed while we wait for pending incoming messages to process"
},
"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"
"savingData": {
"message": "Saving your data",
"description": "Message shown during the upgrade process while we export data"
},
"selectedLocation": {
"message": "your selected location",

View File

@@ -2,36 +2,6 @@
<html>
<head>
<meta charset='utf-8'>
<script type='text/x-tmpl-mustache' id='app-migration-screen'>
<div class='content'>
<img src='/images/icon_128.png'>
{{ ^hideProgress }}
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
{{ /hideProgress }}
<div class='message'>{{& message }}</div>
<div>
{{ #installButton }}
<button class='install grey'>{{ installButton }}</button>
{{ /installButton }}
{{ #nextButton }}
<button class='next grey'>{{ nextButton }}</button>
{{ /nextButton }}
{{ #exportButton }}
<button class='export grey'>{{ exportButton }}</button>
{{ /exportButton }}
{{ #debugLogButton }}
<button class='debug-log grey'>{{ debugLogButton }}</button>
{{ /debugLogButton }}
{{ #cancelButton }}
<button class='cancel grey'>{{ cancelButton }}</button>
{{ /cancelButton }}
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='app-loading-screen'>
<div class='content'>
<img src='/images/icon_128.png'>
@@ -110,12 +80,15 @@
<a target='_blank' href='https://chrome.google.com/webstore/detail/bikioccmkafdpakkkcpdbppfkghcmihk'>
<button class='upgrade'>{{ upgrade }}</button>
</a>
{{ expiredWarning }}
</script>
<script type='text/x-tmpl-mustache' id='migration_alert'>
<button class='migrate'>{{ migrate }}</button>
<div class='message'>
{{ migrationWarning }}
{{ expiredWarning }}
</div>
</script>
<script type='text/x-tmpl-mustache' id='upgrade_banner'>
<span class='x banner-close'></span>
<button class='migrate'>{{ upgradeNow }}</button>
<div class='message'>
{{ upgradeMessage }}
</div>
</script>
<script type='text/x-tmpl-mustache' id='banner'>
@@ -619,6 +592,159 @@
{{/action }}
</script>
<script type='text/x-tmpl-mustache' id='linux-install-instructions'>
<div class='confirmation-dialog modal'>
<div class="content" tabindex="2">
<div class='header'>{{ header }}</div>
<pre class='instructions'>curl -s https://updates.signal.org/desktop/apt/keys.asc | sudo apt-key add -
echo "deb [arch=amd64] https://updates.signal.org/desktop/apt xenial main" | sudo tee -a /etc/apt/sources.list.d/signal-xenial.list
sudo apt update && sudo apt install signal-desktop
/opt/Signal/signal-desktop --import</pre>
<div class='buttons'>
<button class='ok' tabindex='1'>{{ ok }}</button>
</div>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='migration-flow-template'>
{{#isStep1}}
<div id='step1' class='step'>
<div class='inner'>
<div class='step-body'>
<img class='banner-image' src='/images/icon_128.png'>
<div class='header'>{{ startHeader }}</div>
<div class='body-text-wide'>{{ start }}</div>
</div>
<div class='nav'>
<div>
<a class='button start'>{{ startButton }}</a>
</div>
<div>
<a class='link cancel'>{{ cancelButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep1}}
{{#isStep2}}
<div id='step2' class='step'>
<div class='inner'>
<div class='step-body'>
<div class='header'>{{ installHeader }}</div>
<p>{{ install }}</p>
</div>
<div class='install-container'>
<div class='install'>
<a class='install-mac' target='_blank' href='{{ macLink }}'>
<span class='install-icon apple'></span>
<div>Mac</div>
</a>
</div>
<div class='install'>
<a class='install-windows' target='_blank' href='{{ windowsLink }}'>
<span class='install-icon windows'></span>
<div>Windows</div>
</a>
</div>
<div class='install'>
<a class='install-linux' href='#'>
<span class='install-icon linux'></span>
<div>Debian-based Linux</div>
</a>
</div>
</div>
<div class='nav'>
<div>
<a class='button installed'>{{ installCompleteButton }}</a>
</div>
<div>
<a class='link cancel'>{{ cancelButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep2}}
{{#isStep3}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon folder-outline'></span>
<div class='header'>{{ chooseHeader }}</div>
<div class='body-text-wide'>{{ choose }}</div>
</div>
<div class='nav'>
<div>
<a class='button choose'>{{ chooseButton }}</a>
</div>
<div>
<a class='link cancel'>{{ cancelButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep3}}
{{#isStep4}}
<div id='step4' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon export'></span>
<div class='header'>{{ exportHeader }}</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>
<a class='link debug-log'>{{ debugLogButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep4}}
{{#isStep5}}
<div id='step5' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon check-circle-outline'></span>
<div class='header'>{{ completeHeader }}</div>
<div class='body-text-wide center'>{{ completeIntro }}</div>
<div class='export-location'>{{ completeLocation }}</div>
<div class='body-text-wide'>{{ completeNextSteps }}</div>
<div class='body-text-wide'>{{ completeSignoff }}</div>
</div>
<div class='nav'>
<div>
<a class='link debug-log'>{{ debugLogButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep5}}
{{#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'>{{ error }}</div>
</div>
<div class='nav'>
<div>
<a class='button export'>{{ tryAgain }}</a>
</div>
<div>
<a class='link debug-log'>{{ debugLogButton }}</a>
</div>
</div>
</div>
</div>
{{/isError}}
</script>
<script type='text/x-tmpl-mustache' id='install_flow_template'>
<div id='step1' class='step'>
<div class='inner'>

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/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

1
images/export.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="M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z" /></svg>

After

Width:  |  Height:  |  Size: 421 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

2
images/linux.svg Normal file
View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M791 411q-11 1-15.5 10.5t-8.5 9.5q-5 1-5-5 0-12 19-15h10zm87 14q-4 1-11.5-6.5t-17.5-4.5q24-11 32 2 3 6-3 9zm-351 427q-4-1-6 3t-4.5 12.5-5.5 13.5-10 13q-10 11-1 12 4 1 12.5-7t12.5-18q1-3 2-7t2-6 1.5-4.5.5-4v-3l-1-2.5-3-2zm855 359q0-18-55-42 4-15 7.5-27.5t5-26 3-21.5.5-22.5-1-19.5-3.5-22-4-20.5-5-25-5.5-26.5q-10-48-47-103t-72-75q24 20 57 83 87 162 54 278-11 40-50 42-31 4-38.5-18.5t-8-83.5-11.5-107q-9-39-19.5-69t-19.5-45.5-15.5-24.5-13-15-7.5-7q-14-62-31-103t-29.5-56-23.5-33-15-40q-4-21 6-53.5t4.5-49.5-44.5-25q-15-3-44.5-18t-35.5-16q-8-1-11-26t8-51 36-27q37-3 51 30t4 58q-11 19-2 26.5t30 .5q13-4 13-36v-37q-5-30-13.5-50t-21-30.5-23.5-15-27-7.5q-107 8-89 134 0 15-1 15-9-9-29.5-10.5t-33 .5-15.5-5q1-57-16-90t-45-34q-27-1-41.5 27.5t-16.5 59.5q-1 15 3.5 37t13 37.5 15.5 13.5q10-3 16-14 4-9-7-8-7 0-15.5-14.5t-9.5-33.5q-1-22 9-37t34-14q17 0 27 21t9.5 39-1.5 22q-22 15-31 29-8 12-27.5 23.5t-20.5 12.5q-13 14-15.5 27t7.5 18q14 8 25 19.5t16 19 18.5 13 35.5 6.5q47 2 102-15 2-1 23-7t34.5-10.5 29.5-13 21-17.5q9-14 20-8 5 3 6.5 8.5t-3 12-16.5 9.5q-20 6-56.5 21.5t-45.5 19.5q-44 19-70 23-25 5-79-2-10-2-9 2t17 19q25 23 67 22 17-1 36-7t36-14 33.5-17.5 30-17 24.5-12 17.5-2.5 8.5 11q0 2-1 4.5t-4 5-6 4.5-8.5 5-9 4.5-10 5-9.5 4.5q-28 14-67.5 44t-66.5 43-49 1q-21-11-63-73-22-31-25-22-1 3-1 10 0 25-15 56.5t-29.5 55.5-21 58 11.5 63q-23 6-62.5 90t-47.5 141q-2 18-1.5 69t-5.5 59q-8 24-29 3-32-31-36-94-2-28 4-56 4-19-1-18-2 1-4 5-36 65 10 166 5 12 25 28t24 20q20 23 104 90.5t93 76.5q16 15 17.5 38t-14 43-45.5 23q8 15 29 44.5t28 54 7 70.5q46-24 7-92-4-8-10.5-16t-9.5-12-2-6q3-5 13-9.5t20 2.5q46 52 166 36 133-15 177-87 23-38 34-30 12 6 10 52-1 25-23 92-9 23-6 37.5t24 15.5q3-19 14.5-77t13.5-90q2-21-6.5-73.5t-7.5-97 23-70.5q15-18 51-18 1-37 34.5-53t72.5-10.5 60 22.5zm-628-827q3-17-2.5-30t-11.5-15q-9-2-9 7 2 5 5 6 10 0 7 15-3 20 8 20 3 0 3-3zm419 197q-2-8-6.5-11.5t-13-5-14.5-5.5q-5-3-9.5-8t-7-8-5.5-6.5-4-4-4 1.5q-14 16 7 43.5t39 31.5q9 1 14.5-8t3.5-20zm-178-213q0-11-5-19.5t-11-12.5-9-3q-6 0-8 2t0 4 5 3q14 4 18 31 0 3 8-2 2-2 2-3zm54-233q0-2-2.5-5t-9-7-9.5-6q-15-15-24-15-9 1-11.5 7.5t-1 13-.5 12.5q-1 4-6 10.5t-6 9 3 8.5q4 3 8 0t11-9 15-9q1-1 9-1t15-2 9-7zm565 1341q20 12 31 24.5t12 24-2.5 22.5-15.5 22-23.5 19.5-30 18.5-31.5 16.5-32 15.5-27 13q-38 19-85.5 56t-75.5 64q-17 16-68 19.5t-89-14.5q-18-9-29.5-23.5t-16.5-25.5-22-19.5-47-9.5q-44-1-130-1-19 0-57 1.5t-58 2.5q-44 1-79.5 15t-53.5 30-43.5 28.5-53.5 11.5q-29-1-111-31t-146-43q-19-4-51-9.5t-50-9-39.5-9.5-33.5-14.5-17-19.5q-10-23 7-66.5t18-54.5q1-16-4-40t-10-42.5-4.5-36.5 10.5-27q14-12 57-14t60-12q30-18 42-35t12-51q21 73-32 106-32 20-83 15-34-3-43 10-13 15 5 57 2 6 8 18t8.5 18 4.5 17 1 22q0 15-17 49t-14 48q3 17 37 26 20 6 84.5 18.5t99.5 20.5q24 6 74 22t82.5 23 55.5 4q43-6 64.5-28t23-48-7.5-58.5-19-52-20-36.5q-121-190-169-242-68-74-113-40-11 9-15-15-3-16-2-38 1-29 10-52t24-47 22-42q8-21 26.5-72t29.5-78 30-61 39-54q110-143 124-195-12-112-16-310-2-90 24-151.5t106-104.5q39-21 104-21 53-1 106 13.5t89 41.5q57 42 91.5 121.5t29.5 147.5q-5 95 30 214 34 113 133 218 55 59 99.5 163t59.5 191q8 49 5 84.5t-12 55.5-20 22q-10 2-23.5 19t-27 35.5-40.5 33.5-61 14q-18-1-31.5-5t-22.5-13.5-13.5-15.5-11.5-20.5-9-19.5q-22-37-41-30t-28 49 7 97q20 70 1 195-10 65 18 100.5t73 33 85-35.5q59-49 89.5-66.5t103.5-42.5q53-18 77-36.5t18.5-34.5-25-28.5-51.5-23.5q-33-11-49.5-48t-15-72.5 15.5-47.5q1 31 8 56.5t14.5 40.5 20.5 28.5 21 19 21.5 13 16.5 9.5z"/></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

2
images/windows.svg Normal file
View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M746 1006v651l-682-94v-557h682zm0-743v659h-682v-565zm982 743v786l-907-125v-661h907zm0-878v794h-907v-669z"/></svg>

After

Width:  |  Height:  |  Size: 252 B

View File

@@ -69,10 +69,67 @@
return accountManager;
};
function isTimeToUpgrade(text) {
var percentage = parseInt(text, 10);
if (isNaN(percentage)) {
return false;
}
var roll = _.random(1, 100);
return roll <= percentage;
}
var UPGRADE_VALUE = 'feb-2018-upgrade-dice';
var UPGRADE_FLAG = window.UPGRADE_FLAG = 'feb-2018-upgrade-alert';
var UPGRADE_URL = 'https://updates.signal.org/desktop/feb-2018-upgrade.txt';
var HALF_HOUR = 30 * 60 * 1000;
function checkForUpgrade() {
$.ajax(UPGRADE_URL).done(function(data) {
var previous = storage.get(UPGRADE_VALUE);
if (data === previous) {
console.log('Upgrade check: Got same value as last time. Another check in 30 minutes');
return;
}
storage.put(UPGRADE_VALUE, data);
if (isTimeToUpgrade(data)) {
console.log('Upgrade check: time to upgrade!');
storage.put(UPGRADE_FLAG, true);
addUpgradeAlert();
} else {
console.log('Upgrade check: Not yet time to upgrade. Another check in 30 minutes');
setTimeout(checkForUpgrade, HALF_HOUR);
}
}).fail(function() {
console.log('Upgrade check: Request failed; another check in 30 minutes');
setTimeout(checkForUpgrade, HALF_HOUR);
});
}
function addUpgradeAlert() {
var app = window.owsDesktopApp;
if (!app) {
return;
}
var view = app.inboxView;
if (!view) {
return;
}
view.showUpgradeBanner();
}
storage.fetch();
storage.onready(function() {
ConversationController.load();
if (!storage.get(UPGRADE_FLAG)) {
checkForUpgrade();
}
window.dispatchEvent(new Event('storage_ready'));
setUnreadCount(storage.get("unreadCount", 0));

View File

@@ -74,11 +74,11 @@
};
}
function exportNonMessages(idb_db, parent) {
function exportNonMessages(idb_db, parent, options) {
// We wouldn't want to overwrite another db file.
var exclusive = true;
return createFileAndWriter(parent, 'db.json', exclusive).then(function(writer) {
return exportToJsonFile(idb_db, writer);
return exportToJsonFile(idb_db, writer, options);
});
}
@@ -86,10 +86,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');
storeNames = _.without(storeNames, 'messages', 'debug');
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');
@@ -336,11 +353,11 @@
});
}
var attachments = 0;
var failedAttachments = 0;
var numAttachments = 0;
var numFailedAttachments = 0;
function writeAttachment(dir, attachment) {
attachments += 1;
numAttachments += 1;
var filename = getAttachmentFileName(attachment);
// If attachments are in messages with the same received_at and the same name,
@@ -350,7 +367,7 @@
var stream = createOutputStream(writer);
return stream.write(attachment.data);
}).catch(function(error) {
failedAttachments += 1;
numFailedAttachments += 1;
console.log('writeAttachment error:', error && error.stack ? error.stack : error);
});
}
@@ -379,8 +396,8 @@
return filename.toString().replace(/[^a-z0-9.,+()'#\- ]/gi, '_');
}
var conversations = 0;
var failedConversations = 0;
var numConversations = 0;
var numFailedConversations = 0;
function delay(ms) {
console.log('Waiting', ms, 'milliseconds');
@@ -395,7 +412,7 @@
// file for well-formed JSON, trying for up to five minutes.
var CHECK_MAX = 60;
function checkConversation(conversationId, dir, count) {
return delay(5000).then(function() {
return delay(2000).then(function() {
console.log(
'Verifying messages.json produced for conversation',
conversationId,
@@ -414,7 +431,7 @@
);
if (count >= CHECK_MAX) {
failedConversations += 1;
numFailedConversations += 1;
return;
}
@@ -465,14 +482,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(',');
}
// eliminate attachment data from the JSON, since it will go to disk
message.attachments = _.map(attachments, function(attachment) {
return _.omit(attachment, ['data']);
@@ -504,7 +527,7 @@
} else {
var promise = stream.write(']}');
conversations += 1;
numConversations += 1;
promiseChain = promiseChain
.then(promise)
@@ -755,24 +778,25 @@
}
function printAttachmentStats() {
console.log(
'Total attachments:', attachments,
'Failed attachments:', failedAttachments
);
console.log('Total attachments:', numAttachments);
console.log('Failed attachments:', numFailedAttachments);
}
function printConversationStats() {
console.log(
'Total conversations:', conversations,
'Possibly malformed conversations:', failedConversations
);
console.log('Total conversations:', numConversations);
console.log('Possibly malformed conversations:', numFailedConversations);
}
Whisper.Backup = {
backupToDirectory: function() {
exportToDirectory: function(options) {
return getDirectory().then(function(directoryEntry) {
var idb;
var dir;
numConversations = 0;
numFailedConversations = 0;
numAttachments = 0;
numFailedAttachments = 0;
return openDatabase().then(function(idb_db) {
idb = idb_db;
var name = 'Signal Export ' + getTimestamp();
@@ -781,7 +805,7 @@
return createDirectory(directoryEntry, name, exclusive);
}).then(function(directory) {
dir = directory;
return exportNonMessages(idb, dir);
return exportNonMessages(idb, dir, options);
}).then(function() {
return exportConversations(idb, dir);
}).then(function() {
@@ -791,10 +815,10 @@
printAttachmentStats();
printConversationStats();
console.log('done backing up!');
if (failedAttachments) {
if (numFailedAttachments) {
throw new Error('Export failed, one or more attachments failed');
}
if (failedConversations) {
if (numFailedConversations) {
throw new Error('Export failed, one or more conversations failed');
}
return path;

View File

@@ -424,6 +424,6 @@
console.log(event);
};
}
}
},
];
}());

View File

@@ -1,4 +1,4 @@
/*
/*
* vim: ts=4:sw=4:expandtab
*/
(function () {
@@ -50,6 +50,9 @@
this.cancel();
}
},
focusOk: function() {
this.$('.ok').focus();
},
focusCancel: function() {
this.$('.cancel').focus();
}

View File

@@ -145,23 +145,16 @@
this.inboxListView.stopListening();
}.bind(this));
if (extension.expired()) {
var banner = new Whisper.ExpiredAlertBanner().render();
banner.$el.prependTo(this.$el);
this.$el.addClass('expired');
} else if (Whisper.Migration.inProgress()) {
if (Whisper.Migration.inProgress()) {
if (this.appLoadingScreen) {
this.appLoadingScreen.remove();
this.appLoadingScreen = null;
}
this.showMigrationScreen();
this.showUpgradeScreen();
}
else if (storage.get(window.UPGRADE_FLAG)) {
this.showUpgradeBanner();
}
// We'll re-enable this banner when we've made some usability improvements to
// the migration process:
// else {
// var migrationBanner = new Whisper.MigrationAlertBanner().render();
// migrationBanner.$el.prependTo(this.$el);
// }
},
render_attributes: {
welcomeToSignal : i18n('welcomeToSignal'),
@@ -183,12 +176,32 @@
'input input.search': 'filterContacts',
'click .restart-signal': 'reloadBackgroundPage',
'show .lightbox': 'showLightbox',
'click .migrate': 'confirmMigration'
'click .migrate': 'showUpgradeScreen',
'click .banner-close': 'removeUpgradeBanner'
},
confirmMigration: function() {
this.confirm(i18n('confirmMigration'), i18n('migrate')).then(this.showMigrationScreen.bind(this));
showUpgradeBanner: function() {
this.removeUpgradeBanner();
this.upgradeBanner = new Whisper.UpgradeBanner().render();
this.upgradeBanner.$el.prependTo(this.$el);
// This class makes AppView have a height of 100% - 62px, so it doesn't push
// our banner off-screen.
this.$el.addClass('expired');
},
showMigrationScreen: function() {
removeUpgradeBanner: function() {
if (this.upgradeBanner) {
this.upgradeBanner.remove();
this.upgradeBanner = null;
this.$el.removeClass('expired');
}
},
showUpgradeScreen: function() {
if (this.migrationScreen) {
this.migrationScreen.remove();
this.migrationScreen = null;
}
this.migrationScreen = new Whisper.MigrationView();
this.migrationScreen.render();
this.migrationScreen.$el.prependTo(this.el);
@@ -311,13 +324,13 @@
}
});
Whisper.MigrationAlertBanner = Whisper.View.extend({
templateName: 'migration_alert',
className: 'expiredAlert clearfix',
Whisper.UpgradeBanner = Whisper.View.extend({
templateName: 'upgrade_banner',
className: 'expiredAlert upgrade-banner clearfix',
render_attributes: function() {
return {
migrationWarning: i18n('migrationWarning'),
migrate: i18n('migrate'),
upgradeMessage: i18n('upgradeBanner'),
upgradeNow: i18n('upgradeNow'),
};
}
});

View File

@@ -2,13 +2,87 @@
'use strict';
window.Whisper = window.Whisper || {};
var LinuxInstructionsView = Whisper.ConfirmationDialogView.extend({
className: 'linux-install-instructions',
templateName: 'linux-install-instructions',
_super: Whisper.ConfirmationDialogView.prototype,
initialize: function() {
this._super.initialize.call(this, {
okText: i18n('close'),
});
},
events: {
'keyup': 'onKeyup',
'click .ok': 'ok',
'click .modal': 'ok',
},
render_attributes: function() {
var attributes = this._super.render_attributes.call(this);
// TODO: i18n
attributes.header = 'Debian-based Linux install instructions';
return attributes;
},
ok: function(event) {
// We have an event on .modal, which is the background div, darkening the screen.
// This ensures that a click on the dialog will not fire that event, by checking
// the actual thing clicked against the target.
if (event.target !== event.currentTarget) {
return;
}
this._super.ok.call(this);
}
});
var State = {
DISCONNECTING: 1,
EXPORTING: 2,
COMPLETE: 3,
CHOOSE_DIR: 4,
};
var STEPS = {
INTRODUCTION: 1,
INSTALL: 2,
CHOOSE: 3,
EXPORTING: 4,
COMPLETE: 5,
};
var GET_YAML_PATH = /^path: (.+)$/m;
var BASE_PATH = 'https://updates.signal.org/desktop/';
function getCacheBuster() {
return Math.random().toString(36).substring(7);
}
function getLink(file) {
return new Promise(function(resolve, reject) {
$.get(BASE_PATH + file + '?b=' + getCacheBuster())
.done(function(data) {
var match = GET_YAML_PATH.exec(data);
if (match && match[1]) {
return resolve(BASE_PATH + match[1]);
}
return reject(new Error('Link not found in YAML from ' + file + ': ' + data));
})
.fail(function(xhr) {
return reject(new Error(
'Problem pulling ' + file + '; Request Status: ' + xhr.status +
' Status Text: ' + xhr.statusText + ' ' + xhr.responseText)
);
});
});
}
function getMacLink() {
return getLink('latest-mac-import.yml');
}
function getWindowsLink() {
return getLink('latest-import.yml');
}
Whisper.Migration = {
isComplete: function() {
return storage.get('migrationState') === State.COMPLETE;
@@ -32,12 +106,27 @@
},
beginExport: function() {
storage.put('migrationState', State.EXPORTING);
return Whisper.Backup.backupToDirectory();
var everAttempted = this.everAttempted();
storage.put('migrationEverAttempted', true);
// If this is the second time the user is attempting to export, we'll exclude
// client-specific encryption configuration. Yes, this will fire if the user
// initially attempts to save to a read-only directory or something like that, but
// it will prevent the horrible encryption errors which result from import to the
// same client config more than once. They can import the same message history
// more than once, so we preserve that.
return Whisper.Backup.exportToDirectory({
excludeClientConfig: everAttempted,
});
},
init: function() {
storage.put('migrationState', State.DISCONNECTING);
Whisper.events.trigger('start-shutdown');
},
everAttempted: function() {
return Boolean(storage.get('migrationEverAttempted'));
},
everComplete: function() {
return Boolean(storage.get('migrationEverCompleted'));
},
@@ -47,17 +136,42 @@
};
Whisper.MigrationView = Whisper.View.extend({
templateName: 'app-migration-screen',
className: 'app-loading-screen',
templateName: 'migration-flow-template',
className: 'migration-flow',
events: {
'click .install': 'onClickInstall',
'click .export': 'onClickExport',
'click .install-mac': 'onClickMac',
'click .install-windows': 'onClickWindows',
'click .install-linux': 'onClickLinux',
'click .start': 'onClickStart',
'click .installed': 'onClickInstalled',
'click .choose': 'onClickChoose',
'click .debug-log': 'onClickDebugLog',
'click .cancel': 'onClickCancel',
'click .next': 'onClickNext',
},
initialize: function() {
this.step = STEPS.INTRODUCTION;
// init() tells MessageReceiver to disconnect and drain its queue, will fire
// 'shutdown-complete' event when that is done. Might result in a synchronous
// event, so call it after we register our callback.
Whisper.events.once('shutdown-complete', function() {
this.shutdownComplete = true;
}.bind(this));
Whisper.Migration.init();
Promise.all([getMacLink(), getWindowsLink()]).then(function(results) {
this.macLink = results[0];
this.windowsLink = results[1];
this.render();
}.bind(this), function(error) {
console.log(
'MigrationView: Ran into problem pulling Mac/Windows install links:',
error.stack
);
});
if (!Whisper.Migration.inProgress()) {
this.render();
return;
}
@@ -66,107 +180,124 @@
if (Whisper.Migration.everComplete()) {
// If the user has ever successfully exported before, we'll show the 'finished'
// screen with the 'Export again' button.
this.step = STEPS.COMPLETE;
Whisper.Migration.markComplete();
} else if (!Whisper.Migration.isComplete()) {
// This takes the user back to the very beginning of the process.
Whisper.Migration.cancel();
}
this.render();
},
render_attributes: function() {
var message;
var exportButton;
var hideProgress = Whisper.Migration.isComplete();
var debugLogButton = i18n('submitDebugLog');
var installButton = i18n('installNewSignal');
var cancelButton;
var nextButton;
if (this.error) {
// If we've never successfully exported, then we allow user to cancel out
if (!Whisper.Migration.everComplete()) {
cancelButton = i18n('cancel');
}
return {
message: i18n('exportError'),
hideProgress: true,
exportButton: i18n('exportAgain'),
isError: true,
errorHeader: i18n('exportErrorHeader'),
error: i18n('exportError'),
tryAgain: i18n('chooseFolderAndTryAgain'),
debugLogButton: i18n('submitDebugLog'),
cancelButton: cancelButton,
};
}
switch (storage.get('migrationState')) {
case State.COMPLETE:
var location = Whisper.Migration.getExportLocation() || i18n('selectedLocation');
message = i18n('exportComplete', location);
exportButton = i18n('exportAgain');
installButton = null;
debugLogButton = null;
break;
case State.EXPORTING:
message = i18n('exporting');
installButton = null;
break;
case State.DISCONNECTING:
message = i18n('migrationDisconnecting');
installButton = null;
break;
case State.CHOOSE_DIR:
hideProgress = true;
message = i18n('exportInstructions');
exportButton = i18n('export');
debugLogButton = null;
installButton = null;
break;
default:
message = i18n('migrateInstallStep');
hideProgress = true;
debugLogButton = null;
nextButton = i18n('installComplete');
cancelButton = i18n('cancel');
break;
}
var location = Whisper.Migration.getExportLocation() || i18n('selectedLocation');
return {
hideProgress: hideProgress,
message: message,
exportButton: exportButton,
debugLogButton: debugLogButton,
installButton: installButton,
cancelButton: cancelButton,
nextButton: nextButton,
cancelButton: i18n('cancel'),
debugLogButton: i18n('submitDebugLog'),
isStep1: this.step === 1,
startHeader: i18n('startExportHeader'),
start: i18n('startExportIntro'),
startButton: i18n('imReady'),
isStep2: this.step === 2,
installHeader: i18n('installHeader'),
install: i18n('installIntro'),
macOS: i18n('macOS'),
macLink: this.macLink,
windows: i18n('windows'),
windowsLink: this.windowsLink,
linux: i18n('debianLinux'),
installCompleteButton: i18n('installed'),
isStep3: this.step === 3,
chooseHeader: i18n('saveHeader'),
choose: i18n('saveDataPrompt'),
chooseButton: i18n('chooseFolder'),
isStep4: this.step === 4,
exportHeader: i18n('savingData'),
isStep5: this.step === 5,
completeHeader: i18n('completeHeader'),
completeIntro: i18n('completeIntro'),
completeLocation: location,
completeNextSteps: i18n('completeNextSteps'),
completeSignoff: i18n('completeSignoff'),
};
},
onClickInstall: function() {
var url = 'https://support.whispersystems.org/hc/en-us/articles/214507138';
window.open(url, '_blank');
onClickStart: function() {
this.selectStep(STEPS.INSTALL);
},
onClickNext: function() {
storage.put('migrationState', State.CHOOSE_DIR);
onClickMac: function() {
console.log('Mac install link clicked');
},
onClickWindows: function() {
console.log('Windows install link clicked');
},
onClickLinux: function() {
var dialog = this.linuxInstructionsView = new LinuxInstructionsView({});
this.$el.prepend(dialog.el);
dialog.focusOk();
},
onClickInstalled: function() {
this.selectStep(STEPS.CHOOSE);
},
onClickChoose: function() {
this.error = null;
if (!this.shutdownComplete) {
console.log("Preventing export start; we haven't disconnected from the server");
this.error = true;
this.render();
return;
}
if (!Whisper.Migration.everComplete()) {
return this.beginMigration();
}
// Different behavior for the user's second time through
this.selectStep(STEPS.EXPORTING);
Whisper.Migration.beginExport()
.then(this.completeMigration.bind(this))
.catch(function(error) {
if (!error || error.name !== 'ChooseError') {
this.error = error || new Error('in case we reject() null!');
}
// Even if we run into an error, we call this complete because the user has
// completed the process once before.
Whisper.Migration.markComplete();
this.selectStep(STEPS.COMPLETE);
}.bind(this));
this.render();
},
onClickCancel: function() {
this.cancel();
},
onClickDebugLog: function() {
this.openDebugLog();
},
cancel: function() {
console.log('Cancelling out of migration workflow after error');
Whisper.Migration.cancel().then(function() {
console.log('Restarting now');
window.location.reload();
});
},
onClickCancel: function() {
var dialog = new Whisper.ConfirmationDialogView({
message: i18n('cancelWarning'),
okText: i18n('cancelMigration'),
cancelText: i18n('continueMigration'),
resolve: this.cancel.bind(this),
});
selectStep: function(step) {
this.step = step;
this.render();
},
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
onClickDebugLog: function() {
this.openDebugLog();
},
openDebugLog: function() {
this.closeDebugLog();
this.debugLogView = new Whisper.DebugLogView();
@@ -178,49 +309,18 @@
this.debugLogView = null;
}
},
onClickExport: function() {
this.error = null;
if (!Whisper.Migration.everComplete()) {
return this.beginMigration();
}
beginMigration: function() {
this.selectStep(STEPS.EXPORTING);
// Different behavior for the user's second time through
Whisper.Migration.beginExport()
.then(this.completeMigration.bind(this))
.catch(function(error) {
if (!error || error.name !== 'ChooseError') {
this.error = error || new Error('in case we reject() null!');
}
// Even if we run into an error, we call this complete because the user has
// completed the process once before.
Whisper.Migration.markComplete();
this.render();
}.bind(this));
this.render();
},
beginMigration: function() {
Whisper.events.once('shutdown-complete', function() {
Whisper.Migration.beginExport()
.then(this.completeMigration.bind(this))
.catch(this.onError.bind(this));
// Rendering because we're now in the 'exporting' state
this.render();
}.bind(this));
// tells MessageReceiver to disconnect and drain its queue, will fire
// 'shutdown-complete' event when that is done. Might result in a synchronous
// event, so call it after we register our callback.
Whisper.Migration.init();
// Rendering because we're now in the 'disconnected' state
this.render();
.catch(this.onError.bind(this));
},
completeMigration: function(target) {
// This will prevent connection to the server on future app launches
Whisper.Migration.markComplete(target);
this.render();
this.selectStep(STEPS.COMPLETE);
},
onError: function(error) {
if (error && error.name === 'ChooseError') {

View File

@@ -218,7 +218,7 @@ button.hamburger {
}
.dropoff {
outline: solid 1px #2090ea;
outline: solid 1px $blue;
}
$avatar-size: 44px;
@@ -542,6 +542,47 @@ input[type=text], input[type=search], textarea {
}
}
.upgrade-banner {
background: linear-gradient(
to bottom,
rgb(238,238,238) 0%, // (1 - 0.41) * 255 + 0.41 * 213
rgb(243,243,243) 20%, // (1 - 0.19) * 255 + 0.19 * 191
rgb(255,255,255) 35%,
rgb(255,255,255) 65%,
rgb(249,249,249) 80%, // (1 - 0.19) * 255 + 0.19 * 222
rgb(213,213,213) 100% // (1 - 0.27) * 255 + 0.27 * 98
);
padding: 10px;
font-family: roboto-light;
font-size: 14pt;
button {
float: right;
border: none;
color: white;
font-family: roboto-light;
font-size: 14pt;
border-radius: 0;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
line-height: 36px;
padding: 0 2em;
background: $blue;
margin-left: 20px;
}
.message {
padding: 10px 0;
}
.x {
float: right;
margin-left: 0.5em;
margin-top: 0.5em;
background-color: $grey_l2;
}
}
.inbox {
position: relative;
}
@@ -602,6 +643,266 @@ input[type=text], input[type=search], textarea {
}
}
.migration-flow {
z-index: 1000;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
font-family: roboto-light;
color: black;
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;
@media (min-height: 750px) and (min-width: 700px) {
font-size: 14pt;
}
.banner-image {
margin: 1em;
display: none;
@media (min-height: 550px) {
display: inline-block;
height: 10em;
width: 10em;
}
}
.banner-icon {
margin: 1em;
display: none;
// 640px by 338px is the smallest the window can go
@media (min-height: 550px) {
display: inline-block;
height: 10em;
width: 10em;
}
&.folder-outline {
@include color-svg('../images/folder-outline.svg', #DEDEDE);
}
&.export {
@include color-svg('../images/export.svg', #DEDEDE);
}
&.check-circle-outline {
@include color-svg('../images/check-circle-outline.svg', #DEDEDE);
}
&.alert-outline {
@include color-svg('../images/alert-outline.svg', #DEDEDE);
}
}
.os-header: {
margin-top: 2em;
}
.header {
font-weight: normal;
margin-bottom: 1.5em;
font-size: 20pt;
@media (min-height: 750px) and (min-width: 700px) {
font-size: 28pt;
}
}
.export-location {
text-align: center;
font-family: roboto;
border: solid 1px $blue;
color: $blue;
margin-bottom: 1em;
padding: 0.5em;
-webkit-user-select: text;
cursor: text;
}
.center {
text-align: center;
}
.body-text {
margin-bottom: 1em;
max-width: 22em;
text-align: left;
margin-left: auto;
margin-right: auto;
}
.body-text-wide {
margin-bottom: 1em;
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;
}
.linux-install-instructions .content {
max-width: 1300px;
.header {
color: black;
font-size: 120%;
}
pre {
&::-webkit-scrollbar {
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.40);
border-radius: 2;
}
cursor: text;
-webkit-user-select: text;
background-color: black;
color: white;
text-align: left;
padding: 0.5em;
overflow-x: scroll;
}
}
.inner {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
}
.button {
cursor: pointer;
display: inline-block;
border: none;
min-width: 300px;
padding: 0.75em;
margin-top: 1em;
margin-bottom: 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: $blue;
}
.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);
}
}
.install-container {
@media (min-height: 550px) {
margin-top: 4em;
}
text-align: center;
}
.install {
cursor: pointer;
background-color: white;
padding: 0.5em;
margin: 0.5em;
display: inline-block;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
margin-left: 1em;
margin-right: 1em;
a {
color: black;
text-decoration: none;
}
}
.install-icon {
height: 7em;
width: 7em;
vertical-align: text-bottom;
display: inline-block;
margin: 1em;
@media (min-width: 800px) {
margin-left: 2em;
margin-right: 2em;
}
&.apple {
@include color-svg('/images/apple.svg', $blue);
}
&.windows {
@include color-svg('/images/windows.svg', $blue);
}
&.linux {
@include color-svg('/images/linux.svg', $blue);
}
}
.nav {
width: 100%;
bottom: 50px;
margin-top: auto;
padding-bottom: 1em;
padding-left: 20px;
padding-right: 20px;
}
}
//yellow border fix
.inbox:focus {
outline: none;

View File

@@ -5,7 +5,7 @@
.expired {
.conversation-stack, .gutter {
height: calc(100% - 56px);
height: calc(100% - 62px);
}
}

View File

@@ -40,6 +40,23 @@ $text-dark_l2: darken($text-dark, 30%);
background-color: darken($button-dark, 8%);
}
}
.upgrade-banner {
button {
float: right;
border: none;
color: white;
font-family: roboto-light;
font-size: 14pt;
border-radius: 0;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
line-height: 36px;
padding: 0 2em;
background: $blue;
margin-left: 20px;
}
}
.message-detail, .message-container, .conversation,
.discussion-container {
background-color: $grey-dark_l3;

View File

@@ -363,7 +363,7 @@ button.hamburger {
vertical-align: middle;
display: inline-block;
margin: 0 0 0 8px;
width: calc(100% - 44px - 8px - 0.28571em);
width: calc(100% - 44px - 8px - 0.2857142857em);
text-align: left;
cursor: pointer; }
.contact-details p {
@@ -378,7 +378,7 @@ button.hamburger {
text-align: left; }
.contact-details .number {
color: #616161;
font-size: 0.92857em; }
font-size: 0.9285714286em; }
.contact-details.not-clickable {
cursor: default; }
.contact-details .verified-icon {
@@ -485,6 +485,31 @@ input[type=text]:active, input[type=text]:focus, input[type=search]:active, inpu
.expiredAlert .message {
padding: 10px 0; }
.upgrade-banner {
background: linear-gradient(to bottom, #eeeeee 0%, #f3f3f3 20%, white 35%, white 65%, #f9f9f9 80%, #d5d5d5 100%);
padding: 10px;
font-family: roboto-light;
font-size: 14pt; }
.upgrade-banner button {
float: right;
border: none;
color: white;
font-family: roboto-light;
font-size: 14pt;
border-radius: 0;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
line-height: 36px;
padding: 0 2em;
background: #2090ea;
margin-left: 20px; }
.upgrade-banner .message {
padding: 10px 0; }
.upgrade-banner .x {
float: right;
margin-left: 0.5em;
margin-top: 0.5em;
background-color: #d9d9d9; }
.inbox {
position: relative; }
@@ -530,6 +555,204 @@ input[type=text]:active, input[type=text]:focus, input[type=search]:active, inpu
.app-loading-screen .dot:nth-child(3) {
animation: loading 1500ms ease infinite 666ms; }
.migration-flow {
z-index: 1000;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
font-family: roboto-light;
color: black;
background: linear-gradient(to bottom, #eeeeee 0%, #f3f3f3 12%, white 27%, white 60%, #f9f9f9 85%, #d5d5d5 100%);
display: flex;
align-items: center;
text-align: center;
font-size: 10pt;
.os-header-margin-top: 2em; }
@media (min-height: 750px) and (min-width: 700px) {
.migration-flow {
font-size: 14pt; } }
.migration-flow .banner-image {
margin: 1em;
display: none; }
@media (min-height: 550px) {
.migration-flow .banner-image {
display: inline-block;
height: 10em;
width: 10em; } }
.migration-flow .banner-icon {
margin: 1em;
display: none; }
@media (min-height: 550px) {
.migration-flow .banner-icon {
display: inline-block;
height: 10em;
width: 10em; } }
.migration-flow .banner-icon.folder-outline {
-webkit-mask: url("../images/folder-outline.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #DEDEDE; }
.migration-flow .banner-icon.export {
-webkit-mask: url("../images/export.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #DEDEDE; }
.migration-flow .banner-icon.check-circle-outline {
-webkit-mask: url("../images/check-circle-outline.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #DEDEDE; }
.migration-flow .banner-icon.alert-outline {
-webkit-mask: url("../images/alert-outline.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #DEDEDE; }
.migration-flow .header {
font-weight: normal;
margin-bottom: 1.5em;
font-size: 20pt; }
@media (min-height: 750px) and (min-width: 700px) {
.migration-flow .header {
font-size: 28pt; } }
.migration-flow .export-location {
text-align: center;
font-family: roboto;
border: solid 1px #2090ea;
color: #2090ea;
margin-bottom: 1em;
padding: 0.5em;
-webkit-user-select: text;
cursor: text; }
.migration-flow .center {
text-align: center; }
.migration-flow .body-text {
margin-bottom: 1em;
max-width: 22em;
text-align: left;
margin-left: auto;
margin-right: auto; }
.migration-flow .body-text-wide {
margin-bottom: 1em;
max-width: 30em;
text-align: left;
margin-left: auto;
margin-right: auto; }
.migration-flow .step {
height: 100%;
width: 100%;
padding: 70px 0 50px; }
.migration-flow .step-body {
margin-left: auto;
margin-right: auto;
max-width: 35em; }
.migration-flow .linux-install-instructions .content {
max-width: 1300px; }
.migration-flow .linux-install-instructions .content .header {
color: black;
font-size: 120%; }
.migration-flow .linux-install-instructions .content pre {
cursor: text;
-webkit-user-select: text;
background-color: black;
color: white;
text-align: left;
padding: 0.5em;
overflow-x: scroll; }
.migration-flow .linux-install-instructions .content pre::-webkit-scrollbar {
width: 5px;
height: 5px; }
.migration-flow .linux-install-instructions .content pre::-webkit-scrollbar-track {
background: transparent; }
.migration-flow .linux-install-instructions .content pre::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.4);
border-radius: 2; }
.migration-flow .inner {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%; }
.migration-flow .button {
cursor: pointer;
display: inline-block;
border: none;
min-width: 300px;
padding: 0.75em;
margin-top: 1em;
margin-bottom: 1em;
color: white;
background: #2090ea;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
font-size: 12pt; }
@media (min-height: 750px) and (min-width: 700px) {
.migration-flow .button {
font-size: 20pt; } }
.migration-flow a.link {
display: block;
cursor: pointer;
text-decoration: underline;
margin: 0.5em;
color: #2090ea; }
.migration-flow .progress {
text-align: center;
padding: 1em;
width: 80%;
margin: auto; }
.migration-flow .progress .bar-container {
height: 1em;
margin: 1em;
background-color: #f3f3f3; }
.migration-flow .progress .bar {
width: 100%;
height: 100%;
background-color: #a2d2f4;
transition: width 0.25s;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); }
.migration-flow .install-container {
text-align: center; }
@media (min-height: 550px) {
.migration-flow .install-container {
margin-top: 4em; } }
.migration-flow .install {
cursor: pointer;
background-color: white;
padding: 0.5em;
margin: 0.5em;
display: inline-block;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
margin-left: 1em;
margin-right: 1em; }
.migration-flow .install a {
color: black;
text-decoration: none; }
.migration-flow .install-icon {
height: 7em;
width: 7em;
vertical-align: text-bottom;
display: inline-block;
margin: 1em; }
@media (min-width: 800px) {
.migration-flow .install-icon {
margin-left: 2em;
margin-right: 2em; } }
.migration-flow .install-icon.apple {
-webkit-mask: url("/images/apple.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #2090ea; }
.migration-flow .install-icon.windows {
-webkit-mask: url("/images/windows.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #2090ea; }
.migration-flow .install-icon.linux {
-webkit-mask: url("/images/linux.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #2090ea; }
.migration-flow .nav {
width: 100%;
bottom: 50px;
margin-top: auto;
padding-bottom: 1em;
padding-left: 20px;
padding-right: 20px; }
.inbox:focus {
outline: none; }
@@ -841,11 +1064,11 @@ img.emoji.jumbo {
padding: 0 20px;
margin: 0 0 20px 20px; }
.settings .syncSettings .synced_at {
font-size: 0.92857em;
font-size: 0.9285714286em;
color: #616161; }
.settings .syncSettings .sync_failed {
display: none;
font-size: 0.92857em;
font-size: 0.9285714286em;
color: red; }
.conversation-stack,
@@ -853,7 +1076,7 @@ img.emoji.jumbo {
height: 100%; }
.expired .conversation-stack, .expired .gutter {
height: calc(100% - 56px); }
height: calc(100% - 62px); }
.scrollable {
height: 100%;
@@ -994,7 +1217,7 @@ input.search {
padding: 0.5em; }
.index .last-message {
margin: 6px 0 0;
font-size: 0.92857em;
font-size: 0.9285714286em;
font-weight: 300; }
.index .gutter .timestamp {
position: absolute;
@@ -1166,7 +1389,7 @@ input.search {
.key-verification label {
display: block;
margin: 10px 0;
font-size: 0.92857em; }
font-size: 0.9285714286em; }
.key-verification .icon {
height: 1.25em;
width: 1.25em;
@@ -1272,7 +1495,7 @@ input.search {
background-color: white; }
.message-detail .contacts .contact-detail .error-message {
margin: 6px 0 0;
font-size: 0.92857em;
font-size: 0.9285714286em;
font-weight: bold;
color: red; }
.message-detail h3 {
@@ -1654,7 +1877,7 @@ li.entry .error-icon-container {
color: white;
box-shadow: 0 0 5px 0 black;
border-radius: 5px;
font-size: 0.92857em;
font-size: 0.9285714286em;
z-index: 100; }
.confirmation-dialog .content {
@@ -2137,13 +2360,25 @@ li.entry .error-icon-container {
border: 1px solid #292929; }
.android-dark button:hover, .android-dark .confirmation-dialog .content .buttons button:hover {
background-color: #b8b8b8; }
.android-dark .upgrade-banner button {
float: right;
border: none;
color: white;
font-family: roboto-light;
font-size: 14pt;
border-radius: 0;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
line-height: 36px;
padding: 0 2em;
background: #2090ea;
margin-left: 20px; }
.android-dark .message-detail, .android-dark .message-container, .android-dark .conversation,
.android-dark .discussion-container {
background-color: #171717; }
.android-dark .modal .content {
background-color: #333333; }
.android-dark .lightbox .content {
background-color: transparent; }
background-color: rgba(0, 0, 0, 0); }
.android-dark .key-verification .key {
background-color: #030303;
border-color: #292929; }