From ba347744ff0895c055ef2e631be22268624149c1 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 7 Aug 2017 17:24:59 -0700 Subject: [PATCH] Import: choice on first startup, workflow, ported to Node.js fs API FREEBIE --- _locales/en/messages.json | 46 +++- background.html | 75 ++++--- js/background.js | 50 ++++- js/backup.js | 272 ++++++++++++++--------- js/views/app_view.js | 48 +++- js/views/import_view.js | 153 +++++++++++++ js/views/install_choice_view.js | 31 +++ js/views/install_view.js | 8 +- js/views/standalone_registration_view.js | 3 +- preload.js | 4 + stylesheets/manifest.css | 128 +++++------ stylesheets/options.scss | 15 +- 12 files changed, 616 insertions(+), 217 deletions(-) create mode 100644 js/views/import_view.js create mode 100644 js/views/install_choice_view.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 54e5102e9..ef98423ed 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -15,10 +15,18 @@ "message": "Migrate", "description": "Button label to begin migrating this client to Electron" }, - "export": { + "chooseDirectory": { "message": "Choose directory", "description": "Button to allow the user to export all data from app as part of migration process" }, + "exportButton": { + "message": "Export", + "desription": "Button shown on the choose directory dialog which starts the export 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" @@ -53,6 +61,34 @@ "message": "Install new Signal Desktop", "description": "When export is complete, a button shows which sends user to Signal Desktop install instructions" }, + "importButton": { + "message": "Import", + "desription": "Button shown on the choose directory dialog which starts the import process" + }, + "importChooserTitle": { + "message": "Choose directory with exported data", + "description": "Title of the popup window used to select data previously exported" + }, + "importError": { + "message": "Unfortunately, something went wrong during the import. First, make sure all of the originally exported files are available. Then, please submit a debug log so we can help you get migrated!", + "description": "Message shown if the import went wrong." + }, + "tryAgain": { + "message": "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 exported your Signal data. It will be a directory whose name starts with 'Signal Export.'", + "description": "Description of the export process" + }, + "importing": { + "message": "Please wait while we import your data...", + "description": "Shown as we are loading the user's data from disk" + }, + "importComplete": { + "message": "We've successfully loaded your data. The next step is to restart the application!", + "description": "Shown when the import is complete." + }, "selectedLocation": { "message": "your selected location", "description": "Message shown as the export location if we didn't capture the target directory" @@ -442,6 +478,14 @@ "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" + }, + "installImport": { + "message": "Set up with exported data", + "description": "One of two choices presented on the screen shown on first launch" + }, "installGetStartedButton": { "message": "Get started" }, diff --git a/background.html b/background.html index 469005a2b..c9df06964 100644 --- a/background.html +++ b/background.html @@ -22,7 +22,7 @@ @@ -629,8 +629,24 @@ {{/action }} + + + + - diff --git a/js/background.js b/js/background.js index 7258ca52e..c09001ceb 100644 --- a/js/background.js +++ b/js/background.js @@ -53,7 +53,35 @@ }; storage.fetch(); + + // We need this 'first' check because we don't want to start the app up any other time + // than the first time. And storage.fetch() will cause onready() to fire. + var first = true; storage.onready(function() { + if (!first) { + return; + } + first = false; + + start(); + }); + + window.getSyncRequest = function() { + return new textsecure.SyncRequest(textsecure.messaging, messageReceiver); + }; + + Whisper.events.on('shutdown', function() { + if (messageReceiver) { + messageReceiver.close().then(function() { + messageReceiver = null; + Whisper.events.trigger('shutdown-complete'); + }); + } else { + Whisper.events.trigger('shutdown-complete'); + } + }); + + function start() { ConversationController.load(); window.dispatchEvent(new Event('storage_ready')); @@ -61,7 +89,7 @@ console.log("listening for registration events"); Whisper.events.on('registration_done', function() { console.log("handling registration event"); - init(true); + connect(true); }); var appView = window.owsDesktopApp.appView = new Whisper.AppView({el: $('body')}); @@ -70,13 +98,16 @@ Whisper.RotateSignedPreKeyListener.init(Whisper.events); Whisper.ExpiringMessagesListener.init(Whisper.events); - if (Whisper.Registration.everDone()) { - init(); + if (Whisper.Import.isIncomplete()) { + console.log('Import was interrupted, showing import error screen'); + appView.openImporter(); + } else if (Whisper.Registration.everDone()) { + connect(); appView.openInbox({ initialLoadComplete: initialLoadComplete }); } else { - appView.openInstaller(); + appView.openInstallChoice(); } Whisper.events.on('showDebugLog', function() { @@ -109,7 +140,7 @@ }); } }); - }); + } window.getSyncRequest = function() { return new textsecure.SyncRequest(textsecure.messaging, messageReceiver); @@ -126,11 +157,12 @@ } }); - function init(firstRun) { - window.removeEventListener('online', init); + function connect(firstRun) { + window.removeEventListener('online', connect); if (!Whisper.Registration.isDone()) { return; } if (Whisper.Migration.inProgress()) { return; } + if (Whisper.Import.isIncomplete()) { return; } if (messageReceiver) { messageReceiver.close(); } @@ -398,13 +430,13 @@ // Failed to connect to server if (navigator.onLine) { console.log('retrying in 1 minute'); - setTimeout(init, 60000); + setTimeout(connect, 60000); Whisper.events.trigger('reconnectTimer'); } else { console.log('offline'); if (messageReceiver) { messageReceiver.close(); } - window.addEventListener('online', init); + window.addEventListener('online', connect); } return; } diff --git a/js/backup.js b/js/backup.js index b45bae218..48cee70b4 100644 --- a/js/backup.js +++ b/js/backup.js @@ -2,10 +2,12 @@ 'use strict'; window.Whisper = window.Whisper || {}; - function stringToBlob(string) { - var buffer = dcodeIO.ByteBuffer.wrap(string).toArrayBuffer(); - return new Blob([buffer]); - } + var electronRemote = require('electron').remote; + var dialog = electronRemote.dialog; + var BrowserWindow = electronRemote.BrowserWindow; + + var fs = require('fs'); + var path = require('path'); function stringify(object) { for (var key in object) { @@ -41,21 +43,34 @@ return object; } - function createOutputStream(fileWriter) { + function createOutputStream(writer) { var wait = Promise.resolve(); - var count = 0; return { write: function(string) { - var i = count++; wait = wait.then(function() { - return new Promise(function(resolve, reject) { - fileWriter.onwriteend = resolve; - fileWriter.onerror = reject; - fileWriter.onabort = reject; - fileWriter.write(stringToBlob(string)); + return new Promise(function(resolve) { + if (writer.write(string)) { + return resolve(); + } + + // If write() returns true, we don't need to wait for the drain event + // https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable + writer.once('drain', resolve); + + // We don't register for the 'error' event here, only in close(). Otherwise, + // we'll get "Possible EventEmitter memory leak detected" warnings. }); }); return wait; + }, + close: function() { + return wait.then(function() { + return new Promise(function(resolve, reject) { + writer.once('finish', resolve); + writer.once('error', reject); + writer.end(); + }); + }); } }; } @@ -85,7 +100,7 @@ stream.write('{'); _.each(storeNames, function(storeName) { - var transaction = idb_db.transaction(storeNames, "readwrite"); + var transaction = idb_db.transaction(storeNames, 'readwrite'); transaction.onerror = function(error) { console.log( 'exportToJsonFile: transaction error', @@ -129,7 +144,9 @@ stream.write(','); } else { console.log('Exported all stores'); - stream.write('}').then(function() { + stream.write('}'); + + stream.close().then(function() { console.log('Finished writing all stores to disk'); resolve(); }); @@ -158,13 +175,19 @@ var importObject = JSON.parse(jsonString); var storeNames = _.keys(importObject); - console.log('Importing to these stores:', storeNames); + console.log('Importing to these stores:', storeNames.join(', ')); - var transaction = idb_db.transaction(storeNames, "readwrite"); + var transaction = idb_db.transaction(storeNames, 'readwrite'); transaction.onerror = reject; _.each(storeNames, function(storeName) { console.log('Importing items for store', storeName); + + if (!importObject[storeName].length) { + delete importObject[storeName]; + return; + } + var count = 0; _.each(importObject[storeName], function(toAdd) { toAdd = unstringify(toAdd); @@ -218,52 +241,56 @@ } function createDirectory(parent, name) { - var sanitized = sanitizeFileName(name); return new Promise(function(resolve, reject) { - parent.getDirectory(sanitized, {create: true, exclusive: true}, resolve, reject); + var sanitized = sanitizeFileName(name); + var targetDir = path.join(parent, sanitized); + fs.mkdir(targetDir, function(error) { + if (error) { + return reject(error); + } + + return resolve(targetDir); + }); }); } function createFileAndWriter(parent, name) { - var sanitized = sanitizeFileName(name); - return new Promise(function(resolve, reject) { - parent.getFile(sanitized, {create: true, exclusive: true}, function(file) { - return file.createWriter(function(writer) { - resolve(writer); - }, reject); - }, reject); + return new Promise(function(resolve) { + var sanitized = sanitizeFileName(name); + var targetPath = path.join(parent, sanitized); + var options = { + flags: 'wx' + }; + return resolve(fs.createWriteStream(targetPath, options)); }); } function readFileAsText(parent, name) { return new Promise(function(resolve, reject) { - parent.getFile(name, {create: false, exclusive: true}, function(fileEntry) { - fileEntry.file(function(file) { - var reader = new FileReader(); - reader.onload = function(e) { - resolve(e.target.result); - }; - reader.onerror = reject; - reader.onabort = reject; - reader.readAsText(file); - }, reject); - }, reject); + var targetPath = path.join(parent, name); + fs.readFile(targetPath, 'utf8', function(error, string) { + if (error) { + return reject(error); + } + + return resolve(string); + }); }); } function readFileAsArrayBuffer(parent, name) { return new Promise(function(resolve, reject) { - parent.getFile(name, {create: false, exclusive: true}, function(fileEntry) { - fileEntry.file(function(file) { - var reader = new FileReader(); - reader.onload = function(e) { - resolve(e.target.result); - }; - reader.onerror = reject; - reader.onabort = reject; - reader.readAsArrayBuffer(file); - }, reject); - }, reject); + var targetPath = path.join(parent, name); + // omitting the encoding to get a buffer back + fs.readFile(targetPath, function(error, buffer) { + if (error) { + return reject(error); + } + + // Buffer instances are also Uint8Array instances + // https://nodejs.org/docs/latest/api/buffer.html#buffer_buffers_and_typedarray + return resolve(buffer.buffer); + }); }); } @@ -299,15 +326,14 @@ } function readAttachment(parent, message, attachment) { - var name = getAttachmentFileName(attachment); - var sanitized = sanitizeFileName(name); - var attachmentDir = message.received_at; return new Promise(function(resolve, reject) { - parent.getDirectory(attachmentDir, {create: false, exclusive: true}, function(dir) { - return readFileAsArrayBuffer(dir, sanitized ).then(function(contents) { - attachment.data = contents; - return resolve(); - }, reject); + var name = getAttachmentFileName(attachment); + var sanitized = sanitizeFileName(name); + var attachmentDir = path.join(parent, message.received_at.toString()); + + return readFileAsArrayBuffer(attachmentDir, sanitized).then(function(contents) { + attachment.data = contents; + return resolve(); }, reject); }); } @@ -316,7 +342,8 @@ var filename = getAttachmentFileName(attachment); return createFileAndWriter(dir, filename).then(function(writer) { var stream = createOutputStream(writer); - return stream.write(attachment.data); + stream.write(new Buffer(attachment.data)); + return stream.close(); }); } @@ -344,7 +371,7 @@ console.log('exporting conversation', name); return createFileAndWriter(dir, 'messages.json').then(function(writer) { return new Promise(function(resolve, reject) { - var transaction = idb_db.transaction('messages', "readwrite"); + var transaction = idb_db.transaction('messages', 'readwrite'); transaction.onerror = function(e) { console.log( 'exportConversation transaction error for conversation', @@ -406,10 +433,11 @@ count += 1; cursor.continue(); } else { - var promise = stream.write(']}'); - promiseChain = promiseChain.then(promise); + stream.write(']}'); - return promiseChain.then(function() { + var promise = stream.close(); + + return promiseChain.then(promise).then(function() { console.log('done exporting conversation', name); return resolve(); }, function(error) { @@ -457,7 +485,7 @@ function exportConversations(idb_db, parentDir) { return new Promise(function(resolve, reject) { - var transaction = idb_db.transaction('conversations', "readwrite"); + var transaction = idb_db.transaction('conversations', 'readwrite'); transaction.onerror = function(e) { console.log( 'exportConversations: transaction error:', @@ -503,44 +531,40 @@ }); } - function getDirectory() { + function getDirectory(options) { return new Promise(function(resolve, reject) { - var w = extension.windows.getViews()[0]; - if (!w || !w.chrome || !w.chrome.fileSystem) { - return reject(new Error('Ran into problem accessing Chrome filesystem API')); - } + var browserWindow = BrowserWindow.getFocusedWindow(); + var dialogOptions = { + title: options.title, + properties: ['openDirectory'], + buttonLabel: options.buttonLabel + }; - w.chrome.fileSystem.chooseEntry({type: 'openDirectory'}, function(entry) { - if (!entry) { + dialog.showOpenDialog(browserWindow, dialogOptions, function(directory) { + if (!directory || !directory[0]) { var error = new Error('Error choosing directory'); error.name = 'ChooseError'; return reject(error); } - return resolve(entry); + return resolve(directory[0]); }); }); } function getDirContents(dir) { return new Promise(function(resolve, reject) { - var reader = dir.createReader(); - var contents = []; + fs.readdir(dir, function(err, files) { + if (err) { + return reject(err); + } - var getContents = function() { - reader.readEntries(function(results) { - if (results.length) { - contents = contents.concat(results); - getContents(); - } else { - return resolve(contents); - } - }, function(error) { - return reject(error); + files = _.map(files, function(file) { + return path.join(dir, file); }); - }; - getContents(); + resolve(files); + }); }); } @@ -556,10 +580,10 @@ } return new Promise(function(resolve, reject) { - var transaction = idb_db.transaction('messages', "readwrite"); + var transaction = idb_db.transaction('messages', 'readwrite'); transaction.onerror = function(e) { console.log( - 'importConversations transaction error:', + 'saveAllMessages transaction error:', e && e.stack ? e.stack : e ); return reject(e); @@ -614,7 +638,7 @@ return saveAllMessages(idb_db, messages); }); }, function() { - console.log('Warning: could not access messages.json in directory: ' + dir.fullPath); + console.log('Warning: could not access messages.json in directory: ' + dir); }); } @@ -623,7 +647,7 @@ var promiseChain = Promise.resolve(); _.forEach(contents, function(conversationDir) { - if (!conversationDir.isDirectory) { + if (!fs.statSync(conversationDir).isDirectory()) { return; } @@ -638,10 +662,45 @@ }); } - function getDisplayPath(entry) { - return new Promise(function(resolve) { - chrome.fileSystem.getDisplayPath(entry, function(path) { - return resolve(path); + function clearAllStores(idb_db) { + return new Promise(function(resolve, reject) { + console.log('Clearing all indexeddb stores'); + var storeNames = idb_db.objectStoreNames; + var transaction = idb_db.transaction(storeNames, 'readwrite'); + + transaction.oncomplete = function() { + // unused + }; + transaction.onerror = function(error) { + console.log( + 'saveAllMessages transaction error:', + error && error.stack ? error.stack : error + ); + return reject(error); + }; + + var count = 0; + _.forEach(storeNames, function(storeName) { + var store = transaction.objectStore(storeName); + var request = store.clear(); + + request.onsuccess = function() { + count += 1; + console.log('Done clearing store', storeName); + + if (count >= storeNames.length) { + console.log('Done clearing all indexeddb stores'); + return resolve(); + } + }; + + request.onerror = function(error) { + console.log( + 'clearAllStores transaction error:', + error && error.stack ? error.stack : error + ); + return reject(error); + }; }); }); } @@ -651,21 +710,30 @@ } Whisper.Backup = { + clearDatabase: function() { + return openDatabase().then(function(idb_db) { + return clearAllStores(idb_db); + }); + }, backupToDirectory: function() { - return getDirectory().then(function(directoryEntry) { + var options = { + title: i18n('exportChooserTitle'), + buttonLabel: i18n('exportButton'), + }; + return getDirectory(options).then(function(directory) { var idb; var dir; return openDatabase().then(function(idb_db) { idb = idb_db; var name = 'Signal Export ' + getTimestamp(); - return createDirectory(directoryEntry, name); - }).then(function(directory) { - dir = directory; + return createDirectory(directory, name); + }).then(function(created) { + dir = created; return exportNonMessages(idb, dir); }).then(function() { return exportConversations(idb, dir); }).then(function() { - return getDisplayPath(dir); + return dir; }); }).then(function(path) { console.log('done backing up!'); @@ -679,15 +747,19 @@ }); }, importFromDirectory: function() { - return getDirectory().then(function(directoryEntry) { + var options = { + title: i18n('importChooserTitle'), + buttonLabel: i18n('importButton'), + }; + return getDirectory(options).then(function(directory) { var idb; return openDatabase().then(function(idb_db) { idb = idb_db; - return importNonMessages(idb_db, directoryEntry); + return importNonMessages(idb_db, directory); }).then(function() { - return importConversations(idb, directoryEntry); + return importConversations(idb, directory); }).then(function() { - return displayPath(directoryEntry); + return directory; }); }).then(function(path) { console.log('done restoring from backup!'); diff --git a/js/views/app_view.js b/js/views/app_view.js index b17342e9f..b5f1ad5f5 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -29,15 +29,47 @@ 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(); + var importView = this.importView = new Whisper.ImportView(); + this.listenTo(importView, 'cancel', this.openInstallChoice.bind(this)); + this.openView(this.importView); + }, + closeImporter: function() { + if (this.importView) { + this.importView.remove(); + this.importView = null; + } + }, openInstaller: function() { this.closeInstaller(); - this.installView = new Whisper.InstallView(); - if (Whisper.Registration.everDone()) { - this.installView.selectStep(3); - this.installView.hideDots(); - } + this.closeInstallChoice(); + var installView = this.installView = new Whisper.InstallView(); + this.listenTo(installView, 'cancel', this.openInstallChoice.bind(this)); this.openView(this.installView); }, + closeInstaller: function() { + if (this.installView) { + this.installView.remove(); + this.installView = null; + } + }, openStandalone: function() { if (window.config.environment !== 'production') { this.closeInstaller(); @@ -45,12 +77,6 @@ this.openView(this.installView); } }, - closeInstaller: function() { - if (this.installView) { - this.installView.remove(); - this.installView = null; - } - }, openInbox: function(options) { options = options || {}; _.defaults(options, {initialLoadComplete: false}); diff --git a/js/views/import_view.js b/js/views/import_view.js new file mode 100644 index 000000000..8d43ab658 --- /dev/null +++ b/js/views/import_view.js @@ -0,0 +1,153 @@ +/* + * vim: ts=4:sw=4:expandtab + */ +(function () { + 'use strict'; + window.Whisper = window.Whisper || {}; + + var State = { + IMPORTING: 1, + COMPLETE: 2 + }; + + var IMPORT_STARTED = 'importStarted'; + var IMPORT_COMPLETE = 'importComplete'; + var IMPORT_LOCATION = 'importLocation'; + + Whisper.Import = { + isStarted: function() { + return Boolean(storage.get(IMPORT_STARTED)); + }, + isComplete: function() { + return Boolean(storage.get(IMPORT_COMPLETE)); + }, + isIncomplete: function() { + return this.isStarted() && !this.isComplete(); + }, + start: function() { + storage.put(IMPORT_STARTED, true); + }, + complete: function() { + storage.put(IMPORT_COMPLETE, true); + }, + saveLocation: function(location) { + storage.put(IMPORT_LOCATION, location); + }, + reset: function() { + return Whisper.Backup.clearDatabase(); + } + }; + + Whisper.ImportView = Whisper.View.extend({ + templateName: 'app-migration-screen', + className: 'app-loading-screen', + events: { + 'click .import': 'onImport', + 'click .restart': 'onRestart', + 'click .cancel': 'onCancel', + }, + initialize: function() { + if (Whisper.Import.isIncomplete()) { + this.error = true; + } + + this.render(); + 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'), + }; + } + + 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'); + } + + return { + hideProgress: hideProgress, + message: message, + importButton: importButton, + restartButton: restartButton, + cancelButton: cancelButton, + }; + }, + onRestart: function() { + return window.restart(); + }, + onCancel: function() { + this.trigger('cancel'); + }, + onImport: function() { + this.error = null; + + this.state = State.IMPORTING; + this.render(); + + var importLocation; + + // Wait for prior database interaction to complete + this.pending = this.pending.then(function() { + // For resilience to interruptions, clear database both before import and after + return Whisper.Backup.clearDatabase(); + }).then(function() { + Whisper.Import.start(); + return Whisper.Backup.importFromDirectory(); + }).then(function(directory) { + importLocation = 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() { + // Clearing any migration-related state inherited from the Chome App + storage.remove('migrationState'); + storage.remove('migrationEnabled'); + storage.remove('migrationEverCompleted'); + storage.remove('migrationStorageLocation'); + + if (importLocation) { + Whisper.Import.saveLocation(importLocation); + } + + Whisper.Import.complete(); + + this.state = State.COMPLETE; + this.render(); + }.bind(this)).catch(function(error) { + if (error.name !== 'ChooseError') { + this.error = error.message; + console.log('Error importing:', error && error.stack ? error.stack : error); + } + + this.state = null; + this.render(); + + if (this.error) { + return Whisper.Backup.clearDatabase(); + } + }.bind(this)); + } + }); +})(); diff --git a/js/views/install_choice_view.js b/js/views/install_choice_view.js new file mode 100644 index 000000000..49cca2dd7 --- /dev/null +++ b/js/views/install_choice_view.js @@ -0,0 +1,31 @@ +/* + * 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'); + } + }); +})(); diff --git a/js/views/install_view.js b/js/views/install_view.js index 3cf3e9c8e..5992c1634 100644 --- a/js/views/install_view.js +++ b/js/views/install_view.js @@ -7,8 +7,7 @@ Whisper.InstallView = Whisper.View.extend({ templateName: 'install_flow_template', - id: 'install', - className: 'main', + className: 'main install', render_attributes: function() { var twitterHref = 'https://twitter.com/whispersystems'; var signalHref = 'https://signal.org/install'; @@ -48,6 +47,11 @@ this.$('#step1').show(); this.connect(); this.on('disconnected', this.reconnect); + + if (Whisper.Registration.everDone()) { + this.installView.selectStep(3); + this.installView.hideDots(); + } }, connect: function() { this.clearQR(); diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index 6de51eae4..c27bd5e97 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -7,8 +7,7 @@ Whisper.StandaloneRegistrationView = Whisper.View.extend({ templateName: 'standalone', - id: 'install', - className: 'main', + className: 'install main', initialize: function() { this.accountManager = getAccountManager(); diff --git a/preload.js b/preload.js index 58b88392e..aa1adc8dc 100644 --- a/preload.js +++ b/preload.js @@ -1,5 +1,6 @@ (function () { 'use strict'; + console.log('preload'); const electron = require('electron') @@ -88,4 +89,7 @@ }, 30); }); + // we have to pull this in this way because it references node APIs + require('./js/backup'); + })(); diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css index 2bfe1101b..9a7729ac5 100644 --- a/stylesheets/manifest.css +++ b/stylesheets/manifest.css @@ -3330,48 +3330,49 @@ li.entry .error-icon-container { .iti-flag { background: url("../images/flags.png"); } -#install { +.install { height: 100%; background: #2090ea; color: white; text-align: center; font-size: 16px; overflow: auto; } - #install input, #install button, #install select, #install textarea { + .install input, .install button, .install select, .install textarea { font-family: inherit; font-size: inherit; line-height: inherit; } - #install .main { + .install .main { padding: 70px 0 50px; } - #install .step { - display: none; + .install .hidden { + display: none; } + .install .step { height: 100%; } - #install .inner { + .install .inner { display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100%; } - #install .inner .step-body { + .install .inner .step-body { margin-top: auto; width: 100%; max-width: 600px; } - #install #signal-computer, - #install #signal-phone { + .install #signal-computer, + .install #signal-phone { max-width: 50%; max-height: 250px; } - #install p { + .install p { max-width: 35em; margin: 1em auto; padding: 0 1em; line-height: 1.5em; font-size: 1.2em; font-weight: bold; } - #install a { + .install a { cursor: pointer; } - #install a, #install a:visited, #install a:hover { + .install a, .install a:visited, .install a:hover { text-decoration: none; } - #install .button { + .install .button { display: inline-block; text-transform: uppercase; border: none; @@ -3381,14 +3382,14 @@ li.entry .error-icon-container { margin: 0.5em 0; background: white; color: #2090ea; } - #install .nav { + .install .nav { width: 100%; bottom: 50px; margin-top: auto; padding: 20px; } - #install .nav .button { - margin-bottom: 3em; } - #install .nav .dot { + .install .nav .dot-container { + margin-top: 3em; } + .install .nav .dot { display: inline-block; cursor: pointer; margin: 10px; @@ -3397,40 +3398,43 @@ li.entry .error-icon-container { border-radius: 10px; background: white; border: solid 5px #2090ea; } - #install .nav .dot.selected { + .install .nav .dot.selected { background: #a2d2f4; } - #install .link:hover, #install .link:focus { + .install.install-choice .nav { + top: 20px; + margin-bottom: auto; } + .install .link:hover, .install .link:focus { background: rgba(255, 255, 255, 0.3); outline: none; } - #install .link, #install .link:visited, #install .link:hover { + .install .link, .install .link:visited, .install .link:hover { padding: 0 3px; color: white; font-weight: bold; border-bottom: dashed 2px white; text-decoration: none; } - #install .container { + .install .container { min-width: 650px; } - #install h1 { + .install h1 { font-size: 30pt; font-weight: normal; padding-bottom: 10px; } - #install h3.step { + .install h3.step { margin-top: 0; font-weight: bold; } - #install .help { + .install .help { border-top: 2px solid #f3f3f3; padding: 1.5em 0.1em; } - #install .install { + .install .install { display: inline-block; margin-top: 90px; } - #install #qr { + .install #qr { display: inline-block; min-height: 266px; } - #install #qr img { + .install #qr img { border: 5px solid white; } - #install #qr canvas { + .install #qr canvas { display: none; } - #install #device-name { + .install #device-name { border: none; border-bottom: 1px solid white; padding: 8px; @@ -3438,40 +3442,40 @@ li.entry .error-icon-container { color: white; font-weight: bold; text-align: center; } - #install #device-name::selection, #install #device-name a::selection { + .install #device-name::selection, .install #device-name a::selection { color: #454545; background: white; } - #install #device-name::-moz-selection, #install #device-name a::-moz-selection { + .install #device-name::-moz-selection, .install #device-name a::-moz-selection { color: #454545; background: white; } - #install #device-name:focus { + .install #device-name:focus { outline: none; } - #install #device-name:hover, #install #device-name:focus { + .install #device-name:hover, .install #device-name:focus { background: rgba(255, 255, 255, 0.1); } - #install #verifyCode, - #install #code, - #install #number { + .install #verifyCode, + .install #code, + .install #number { box-sizing: border-box; width: 100%; display: block; margin-bottom: 0.5em; text-align: center; } - #install #request-voice, - #install #request-sms { + .install #request-voice, + .install #request-sms { box-sizing: border-box; } - #install #request-sms { + .install #request-sms { width: 57%; float: right; } - #install #request-voice { + .install #request-voice { width: 40%; float: left; } - #install .number-container { + .install .number-container { position: relative; margin-bottom: 0.5em; } - #install .number-container .intl-tel-input, - #install .number-container .number { + .install .number-container .intl-tel-input, + .install .number-container .number { width: 100%; } - #install .number-container::after { + .install .number-container::after { visibility: hidden; content: ' '; display: inline-block; @@ -3485,61 +3489,61 @@ li.entry .error-icon-container { left: 100%; margin: 3px 8px; text-align: center; } - #install .number-container.valid::after { + .install .number-container.valid::after { visibility: visible; content: '✓'; background-color: #0f9d58; color: #ffffff; } - #install .number-container.invalid::after { + .install .number-container.invalid::after { visibility: visible; content: '!'; background-color: #f44336; color: #ffffff; } - #install #error { + .install #error { color: white; font-weight: bold; padding: 0.5em; text-align: center; } - #install #error { + .install #error { background-color: #f44336; } - #install #error:before { + .install #error:before { content: '\26a0'; padding-right: 0.5em; } - #install .narrow { + .install .narrow { margin: auto; box-sizing: border-box; width: 275px; max-width: 100%; } - #install ul.country-list { + .install ul.country-list { min-width: 197px !important; } - #install .confirmation-dialog, #install .progress-dialog, #install .error-dialog { + .install .confirmation-dialog, .install .progress-dialog, .install .error-dialog { padding: 1em; text-align: left; } - #install .number { + .install .number { text-align: center; } - #install .confirmation-dialog button, #install .error-dialog button { + .install .confirmation-dialog button, .install .error-dialog button { float: right; margin-left: 10px; } - #install .progress-dialog { + .install .progress-dialog { text-align: center; padding: 1em; width: 100%; max-width: 600px; margin: auto; } - #install .progress-dialog .status { + .install .progress-dialog .status { padding: 1em; } - #install .progress-dialog .bar-container { + .install .progress-dialog .bar-container { height: 1em; background-color: #f3f3f3; border: solid 1px white; } - #install .progress-dialog .bar { + .install .progress-dialog .bar { width: 0; height: 100%; background-color: #a2d2f4; transition: width 0.25s; } - #install .error-dialog { + .install .error-dialog { display: none; } - #install .modal-container { + .install .modal-container { display: none; position: absolute; width: 100%; @@ -3548,7 +3552,7 @@ li.entry .error-icon-container { top: 0; padding-top: 10em; text-align: center; } - #install .modal-container .modal-main { + .install .modal-container .modal-main { display: inline-block; width: 80%; max-width: 500px; @@ -3556,7 +3560,7 @@ li.entry .error-icon-container { background: white; margin: 10% auto; box-shadow: 0 0 5px 3px rgba(10, 62, 103, 0.2); } - #install .modal-container .modal-main h4 { + .install .modal-container .modal-main h4 { background-color: #2090ea; color: white; padding: 1em; diff --git a/stylesheets/options.scss b/stylesheets/options.scss index ff1e91757..d62b1abe2 100644 --- a/stylesheets/options.scss +++ b/stylesheets/options.scss @@ -6,7 +6,7 @@ background: url("../images/flags.png"); } -#install { +.install { height: 100%; background: #2090ea; color: white; @@ -23,8 +23,10 @@ .main { padding: 70px 0 50px; } - .step { + .hidden { display: none; + } + .step { height: 100%; } .inner { @@ -81,8 +83,8 @@ margin-top: auto; padding: 20px; - .button { - margin-bottom: 3em; + .dot-container { + margin-top: 3em; } .dot { @@ -101,6 +103,11 @@ } } + &.install-choice .nav { + top: 20px; + margin-bottom: auto; + } + .link { &:hover, &:focus { background: rgba(255,255,255,0.3);