Browse Source

Add "device unreachable" dialog and improve welcome page

The texts of the welcome page have been updated and are being mostly
reused for the "device unreachable" dialog.

Further changes:

- Removed fixed height requirement for the welcome page.
- Removed dead code in welcome page CSS
Lennart Grahl 6 years ago
parent
commit
16ca4cd482

+ 7 - 0
public/i18n/de.json

@@ -8,6 +8,7 @@
         "PLEASE_SCAN": "Scannen Sie den QR-Code mit Ihrer Threema-App",
         "PLEASE_SCAN": "Scannen Sie den QR-Code mit Ihrer Threema-App",
         "PLEASE_UNLOCK": "Verbindung wiederaufbauen",
         "PLEASE_UNLOCK": "Verbindung wiederaufbauen",
         "WAITING": "Auf Verbindung warten",
         "WAITING": "Auf Verbindung warten",
+        "RETRY": "Erneut versuchen",
         "PLEASE_RELOAD": "Bitte laden Sie die Seite neu.",
         "PLEASE_RELOAD": "Bitte laden Sie die Seite neu.",
         "RELOAD": "Seite neu laden",
         "RELOAD": "Seite neu laden",
         "PASSWORD": "Passwort",
         "PASSWORD": "Passwort",
@@ -59,6 +60,8 @@
         "APP_STARTED": "Ist die Threema-App gestartet?",
         "APP_STARTED": "Ist die Threema-App gestartet?",
         "SESSION_DELETED": "Wurde diese Sitzung in der Threema-App gelöscht?",
         "SESSION_DELETED": "Wurde diese Sitzung in der Threema-App gelöscht?",
         "PHONE_ONLINE": "Ist Ihr Mobilgerät mit dem Internet verbunden?",
         "PHONE_ONLINE": "Ist Ihr Mobilgerät mit dem Internet verbunden?",
+        "UNLOCK_OR_CHARGE": "Es kann helfen, Ihr Mobilgerät zu entsperren oder an ein Ladegerät anzuschliessen.",
+        "PUSH_FAQ": "Möglicherweise liegt ein Problem bei der Verarbeitung von Push-Benachrichtigungen vor. Die FAQ-Artikel für <a target=\"_blank\" href=\"https://threema.ch/faq/push_andr\">Android</a> und <a target=\"_blank\" href=\"https://threema.ch/faq/push_ios\">iOS</a> helfen bei der Fehlersuche.",
         "WEBCLIENT_ENABLED": "Ist Threema Web in der Threema-App aktiviert?",
         "WEBCLIENT_ENABLED": "Ist Threema Web in der Threema-App aktiviert?",
         "PLUGIN": "Ist in Ihrem Browser ein Plugin zum Blockieren von WebRTC installiert?",
         "PLUGIN": "Ist in Ihrem Browser ein Plugin zum Blockieren von WebRTC installiert?",
         "ADBLOCKER": "Ist in Ihrem Browser ein Ad-Blocker installiert?",
         "ADBLOCKER": "Ist in Ihrem Browser ein Ad-Blocker installiert?",
@@ -290,6 +293,10 @@
             "PLAY_SOUND": "Ton abspielen"
             "PLAY_SOUND": "Ton abspielen"
         }
         }
     },
     },
+    "deviceUnreachable": {
+        "DEVICE_UNREACHABLE": "Mobilgerät nicht erreichbar",
+        "UNABLE_TO_CONNECT": "Eine Verbindung mit Ihrem Mobilgerät konnte nicht hergestellt werden …"
+    },
     "version": {
     "version": {
         "NEW_VERSION": "Neue Version Verfügbar",
         "NEW_VERSION": "Neue Version Verfügbar",
         "NEW_VERSION_BODY": "Eine neue Version von Threema Web ({version}) ist verfügbar. Mehr Informationen finden Sie im {changelog}. Drücken Sie \"OK\", um das Update zu aktivieren."
         "NEW_VERSION_BODY": "Eine neue Version von Threema Web ({version}) ist verfügbar. Mehr Informationen finden Sie im {changelog}. Drücken Sie \"OK\", um das Update zu aktivieren."

+ 7 - 0
public/i18n/en.json

@@ -8,6 +8,7 @@
         "PLEASE_SCAN": "Scan this QR code with your Threema app",
         "PLEASE_SCAN": "Scan this QR code with your Threema app",
         "PLEASE_UNLOCK": "Reconnecting session",
         "PLEASE_UNLOCK": "Reconnecting session",
         "WAITING": "Waiting for connection",
         "WAITING": "Waiting for connection",
+        "RETRY": "Retry",
         "PLEASE_RELOAD": "Please reload the page to try again.",
         "PLEASE_RELOAD": "Please reload the page to try again.",
         "RELOAD": "Reload page",
         "RELOAD": "Reload page",
         "PASSWORD": "Password",
         "PASSWORD": "Password",
@@ -59,6 +60,8 @@
         "APP_STARTED": "Is the Threema app started?",
         "APP_STARTED": "Is the Threema app started?",
         "SESSION_DELETED": "Did you delete this session on your phone?",
         "SESSION_DELETED": "Did you delete this session on your phone?",
         "PHONE_ONLINE": "Is your phone connected to the internet?",
         "PHONE_ONLINE": "Is your phone connected to the internet?",
+        "UNLOCK_OR_CHARGE": "It may help to unlock your device or connect it to a charger.",
+        "PUSH_FAQ": "Your device may be affected by Push Notification issues. See the FAQ articles for <a target=\"_blank\" href=\"https://threema.ch/faq/push_andr\">Android</a> and <a target=\"_blank\" href=\"https://threema.ch/faq/push_ios\">iOS</a> for troubleshooting.",
         "WEBCLIENT_ENABLED": "Is Threema Web enabled in the Threema app?",
         "WEBCLIENT_ENABLED": "Is Threema Web enabled in the Threema app?",
         "PLUGIN": "Is a privacy plugin installed in your browser which blocks WebRTC communication?",
         "PLUGIN": "Is a privacy plugin installed in your browser which blocks WebRTC communication?",
         "ADBLOCKER": "Do you use an ad blocker which also blocks WebRTC communication?",
         "ADBLOCKER": "Do you use an ad blocker which also blocks WebRTC communication?",
@@ -290,6 +293,10 @@
             "PLAY_SOUND": "Play sound"
             "PLAY_SOUND": "Play sound"
         }
         }
     },
     },
+    "deviceUnreachable": {
+        "DEVICE_UNREACHABLE": "Device Unreachable",
+        "UNABLE_TO_CONNECT": "Unable to connect to your device …"
+    },
     "version": {
     "version": {
         "NEW_VERSION": "New Version Available",
         "NEW_VERSION": "New Version Available",
         "NEW_VERSION_BODY": "A new version of Threema Web ({version}) is available. Check out the {changelog} for more information. Click \"OK\" to activate the update."
         "NEW_VERSION_BODY": "A new version of Threema Web ({version}) is available. Check out the {changelog} for more information. Click \"OK\" to activate the update."

+ 43 - 0
src/partials/dialog.device-unreachable.html

@@ -0,0 +1,43 @@
+<md-dialog aria-label="Device Unreachable">
+    <form ng-cloak>
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>deviceUnreachable.DEVICE_UNREACHABLE</h2>
+            </div>
+        </md-toolbar>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <h3 translate>deviceUnreachable.UNABLE_TO_CONNECT</h3>
+                <ul class="material-icons-list">
+                    <li class="help">
+                        <span translate>troubleshooting.PHONE_ONLINE</span>
+                    </li>
+                    <li class="help">
+                        <span translate>troubleshooting.APP_STARTED</span>
+                    </li>
+                    <li class="help">
+                        <span translate>troubleshooting.WEBCLIENT_ENABLED</span>
+                    </li>
+                    <li class="info">
+                        <span translate>troubleshooting.UNLOCK_OR_CHARGE</span>
+                    </li>
+                    <li class="info">
+                        <span translate>troubleshooting.PUSH_FAQ</span>
+                    </li>
+                </ul>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button role="button" class="md-primary reload-btn" ng-click="ctrl.reload()" aria-labelledby="aria-label-reload">
+                <span translate id="aria-label-reload">welcome.RELOAD</span>
+            </md-button>
+            <md-button role="button" class="md-primary reload-btn circular-progress-button" ng-click="ctrl.retry()" ng-disabled="ctrl.retrying" aria-labelledby="aria-label-retry">
+                <md-progress-circular ng-if="ctrl.retrying" md-mode="determinate" md-diameter="20" value="{{ctrl.progress}}"></md-progress-circular>
+                <i ng-if="!ctrl.retrying" class="material-icons">refresh</i>
+                <span translate id="aria-label-retry">welcome.RETRY</span>
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
+

+ 52 - 4
src/partials/messenger.ts

@@ -24,7 +24,7 @@ import {
 } from '@uirouter/angularjs';
 } from '@uirouter/angularjs';
 
 
 import {ContactControllerModel} from '../controller_model/contact';
 import {ContactControllerModel} from '../controller_model/contact';
-import {bufferToUrl, filter, hasValue, logAdapter, supportsPassive, throttle, u8aToHex} from '../helpers';
+import {bufferToUrl, hasValue, logAdapter, supportsPassive, throttle, u8aToHex} from '../helpers';
 import {emojify} from '../helpers/emoji';
 import {emojify} from '../helpers/emoji';
 import {ContactService} from '../services/contact';
 import {ContactService} from '../services/contact';
 import {ControllerService} from '../services/controller';
 import {ControllerService} from '../services/controller';
@@ -61,7 +61,7 @@ class DialogController {
         this.done();
         this.done();
     }
     }
 
 
-    protected hide(data: any): void {
+    protected hide(data?: any): void {
         this.$mdDialog.hide(data);
         this.$mdDialog.hide(data);
         this.done();
         this.done();
     }
     }
@@ -127,11 +127,60 @@ class SendFileController extends DialogController {
     }
     }
 }
 }
 
 
+/**
+ * Handle device unreachable
+ */
+export class DeviceUnreachableController extends DialogController {
+    public static readonly $inject = ['$rootScope', '$window', '$mdDialog', 'CONFIG', 'WebClientService'];
+    private readonly $rootScope: any;
+    private readonly $window: ng.IWindowService;
+    private readonly webClientService: WebClientService;
+    public retrying: boolean = false;
+    public progress: number = 0;
+
+    constructor(
+        $rootScope: any, $window: ng.IWindowService, $mdDialog: ng.material.IDialogService,
+        CONFIG: threema.Config, webClientService: WebClientService,
+    ) {
+        super($mdDialog, CONFIG);
+        this.$rootScope = $rootScope;
+        this.$window = $window;
+        this.webClientService = webClientService;
+    }
+
+    /**
+     * Retry wakeup of the device via a push session.
+     */
+    public async retry(): Promise<any> {
+        // Schedule sending a push
+        const [expectedPeriodMaxMs, pushSessionPromise] = this.webClientService.sendPush();
+
+        // Initialise progress circle
+        this.retrying = true;
+        this.progress = 0;
+        const interval = setInterval(() => this.$rootScope.$apply(() => ++this.progress), expectedPeriodMaxMs / 100);
+
+        // Wait for push to succeed/reject and reset the progress circle
+        try {
+            await pushSessionPromise;
+        } finally {
+            clearInterval(interval);
+            this.$rootScope.$apply(() => this.retrying = false);
+        }
+    }
+
+    /**
+     * Reload the page.
+     */
+    public reload(): void {
+        this.$window.location.reload();
+    }
+}
+
 /**
 /**
  * Handle settings
  * Handle settings
  */
  */
 class SettingsController {
 class SettingsController {
-
     public static $inject = ['$mdDialog', '$window', 'SettingsService', 'NotificationService'];
     public static $inject = ['$mdDialog', '$window', 'SettingsService', 'NotificationService'];
 
 
     public $mdDialog: ng.material.IDialogService;
     public $mdDialog: ng.material.IDialogService;
@@ -190,7 +239,6 @@ class SettingsController {
     public setWantsSound(notificationSound: boolean) {
     public setWantsSound(notificationSound: boolean) {
         this.notificationService.setWantsSound(notificationSound);
         this.notificationService.setWantsSound(notificationSound);
     }
     }
-
 }
 }
 
 
 interface ConversationStateParams extends UiStateParams {
 interface ConversationStateParams extends UiStateParams {

+ 13 - 13
src/partials/welcome.html

@@ -66,29 +66,29 @@
                     <p ng-if="ctrl.state === 'loading' || ctrl.state === 'done'" translate>welcome.LOADING_INITIAL_DATA</p>
                     <p ng-if="ctrl.state === 'loading' || ctrl.state === 'done'" translate>welcome.LOADING_INITIAL_DATA</p>
                     <div class="troubleshoot" ng-if="ctrl.slowConnect">
                     <div class="troubleshoot" ng-if="ctrl.slowConnect">
                         <h3 translate>troubleshooting.SLOW_CONNECT</h3>
                         <h3 translate>troubleshooting.SLOW_CONNECT</h3>
-                        <ul>
-                            <li>
-                                <i class="material-icons md-dark md-14">help</i>
+                        <ul class="material-icons-list">
+                            <li class="help">
                                 <span translate>troubleshooting.PHONE_ONLINE</span>
                                 <span translate>troubleshooting.PHONE_ONLINE</span>
                             </li>
                             </li>
-                            <li ng-if="ctrl.state === 'push'">
-                                <i class="material-icons md-dark md-14">help</i>
+                            <li ng-if="ctrl.state === 'push'" class="help">
                                 <span translate>troubleshooting.APP_STARTED</span>
                                 <span translate>troubleshooting.APP_STARTED</span>
                             </li>
                             </li>
-                            <li ng-if="ctrl.state === 'push'">
-                                <i class="material-icons md-dark md-14">help</i>
+                            <li ng-if="ctrl.state === 'push'" class="help">
                                 <span translate>troubleshooting.WEBCLIENT_ENABLED</span>
                                 <span translate>troubleshooting.WEBCLIENT_ENABLED</span>
                             </li>
                             </li>
-                            <li ng-if="ctrl.state === 'push'">
-                                <i class="material-icons md-dark md-14">help</i>
+                            <li ng-if="ctrl.state === 'push'" class="help">
                                 <span translate>troubleshooting.SESSION_DELETED</span>
                                 <span translate>troubleshooting.SESSION_DELETED</span>
                             </li>
                             </li>
-                            <li ng-if="ctrl.state === 'peer_handshake' && ctrl.showWebrtcTroubleshooting">
-                                <i class="material-icons md-dark md-14">help</i>
+                            <li ng-if="ctrl.state === 'push'" class="info">
+                                <span translate>troubleshooting.UNLOCK_OR_CHARGE</span>
+                            </li>
+                            <li ng-if="ctrl.state === 'push'" class="info">
+                                <span translate>troubleshooting.PUSH_FAQ</span>
+                            </li>
+                            <li ng-if="ctrl.state === 'peer_handshake' && ctrl.showWebrtcTroubleshooting" class="help">
                                 <span translate>troubleshooting.PLUGIN</span>
                                 <span translate>troubleshooting.PLUGIN</span>
                             </li>
                             </li>
-                            <li ng-if="ctrl.state === 'peer_handshake' && ctrl.showWebrtcTroubleshooting">
-                                <i class="material-icons md-dark md-14">help</i>
+                            <li ng-if="ctrl.state === 'peer_handshake' && ctrl.showWebrtcTroubleshooting" class="help">
                                 <span translate>troubleshooting.ADBLOCKER</span>
                                 <span translate>troubleshooting.ADBLOCKER</span>
                             </li>
                             </li>
                         </ul>
                         </ul>

+ 32 - 0
src/sass/layout/_main.scss

@@ -250,7 +250,32 @@
             opacity: 1;
             opacity: 1;
         }
         }
     }
     }
+}
+
+ul.material-icons-list {
+    list-style-type: none;
+
+    li {
+        $list-style-width: 1.3em;
+        margin-left: $list-style-width;
+        text-indent: -$list-style-width;
+
+        &::before {
+            font-family: 'Material Icons';
+        }
+
+        &.help {
+            &::before {
+                content: 'help';
+            }
+        }
 
 
+        &.info {
+            &::before {
+                content: 'info';
+            }
+        }
+    }
 }
 }
 
 
 .md-dialog-content {
 .md-dialog-content {
@@ -259,6 +284,13 @@
     }
     }
 }
 }
 
 
+.circular-progress-button {
+    md-progress-circular {
+        float: left;
+        margin: 8px 4px;
+    }
+}
+
 input.threema-id {
 input.threema-id {
     text-transform: uppercase;
     text-transform: uppercase;
 }
 }

+ 10 - 33
src/sass/sections/_welcome.scss

@@ -112,10 +112,13 @@
     }
     }
 
 
     .loading {
     .loading {
+        $progress-height: 250px;
+        $progress-overlap: 70px;
         margin-top: 48px;
         margin-top: 48px;
 
 
         md-progress-circular {
         md-progress-circular {
-            margin: 0 auto;
+            margin: 0 auto calc(-#{$progress-height} + #{$progress-overlap});
+            height: $progress-height;
 
 
             svg path {
             svg path {
                 stroke-width: 12px !important;
                 stroke-width: 12px !important;
@@ -123,13 +126,6 @@
         }
         }
 
 
         .info {
         .info {
-            display: flex;
-            position: relative;
-            top: -250px;
-            flex-direction: column;
-            justify-content: center;
-            height: 250px;
-
             .percentage {
             .percentage {
                 margin-bottom: 8px;
                 margin-bottom: 8px;
                 vertical-align: center;
                 vertical-align: center;
@@ -140,11 +136,7 @@
         }
         }
 
 
         .troubleshoot {
         .troubleshoot {
-            $troubleshoot-height: 190px;
-            position: absolute;
-            bottom: -$troubleshoot-height - 32px;
-            width: 100%;
-            height: $troubleshoot-height;
+            margin-top: calc(#{$progress-overlap} + 40px);
 
 
             h3 {
             h3 {
                 margin-bottom: 8px;
                 margin-bottom: 8px;
@@ -152,13 +144,13 @@
             }
             }
 
 
             ul {
             ul {
+                text-align: left;
                 font-size: .9em;
                 font-size: .9em;
-                list-style-type: none;
-            }
 
 
-            li {
-                padding-bottom: .3em;
-                line-height: 1.2em;
+                li {
+                    padding: 0 1em .3em;
+                    line-height: 1.2em;
+                }
             }
             }
 
 
             .forget {
             .forget {
@@ -172,19 +164,4 @@
             }
             }
         }
         }
     }
     }
-
-    .notification {
-        flex-direction: horizontal;
-        margin-bottom: 16px;
-        background-color: #ff9800;
-        padding: 8px;
-
-        p {
-            width: 100%;
-            text-align: center;
-            line-height: 1.4em;
-            font-size: .8em;
-            font-weight: bold;
-        }
-    }
 }
 }

+ 49 - 21
src/services/webclient.ts

@@ -46,6 +46,7 @@ import {TitleService} from './title';
 import {VersionService} from './version';
 import {VersionService} from './version';
 
 
 import {TimeoutError} from '../exceptions';
 import {TimeoutError} from '../exceptions';
+import {DeviceUnreachableController} from '../partials/messenger';
 import {ChunkCache} from '../protocol/cache';
 import {ChunkCache} from '../protocol/cache';
 import {SequenceNumber} from '../protocol/sequence_number';
 import {SequenceNumber} from '../protocol/sequence_number';
 
 
@@ -217,11 +218,12 @@ export class WebClientService {
     public alerts: threema.Alert[] = [];
     public alerts: threema.Alert[] = [];
 
 
     // Push
     // Push
+    private readonly pushExpectedPeriodMaxMs: number = PushSession.expectedPeriodMaxMs();
     private pushToken: string = null;
     private pushToken: string = null;
     private pushTokenType: threema.PushTokenType = null;
     private pushTokenType: threema.PushTokenType = null;
-    private pushSession: PushSession = null;
-    private pushPromise: Promise<any> = null;
-    private readonly pushExpectedPeriodMaxMs: number = PushSession.expectedPeriodMaxMs();
+    private pushSession: PushSession | null = null;
+    private pushPromise: Promise<any> | null = null;
+    private deviceUnreachableDialog: ng.IPromise<any> | null = null;
 
 
     // Timeouts
     // Timeouts
     private batteryStatusTimeout: ng.IPromise<void> = null;
     private batteryStatusTimeout: ng.IPromise<void> = null;
@@ -490,10 +492,7 @@ export class WebClientService {
         this.salty.on('new-responder', () => {
         this.salty.on('new-responder', () => {
             if (!this.startupDone) {
             if (!this.startupDone) {
                 // Pushing complete
                 // Pushing complete
-                if (this.pushSession !== null) {
-                    this.pushSession.done();
-                    this.pushSession = null;
-                }
+                this.resetPushSession(true);
 
 
                 // Peer handshake
                 // Peer handshake
                 this.stateService.updateConnectionBuildupState('peer_handshake');
                 this.stateService.updateConnectionBuildupState('peer_handshake');
@@ -1038,16 +1037,30 @@ export class WebClientService {
         if (this.pushSession === null) {
         if (this.pushSession === null) {
             this.pushSession = this.pushService.createSession(this.salty.permanentKeyBytes);
             this.pushSession = this.pushService.createSession(this.salty.permanentKeyBytes);
 
 
-            // Start and handle errors
-            this.pushPromise = this.pushSession.start().catch((error) => {
-                if (error instanceof TimeoutError) {
-                    // TODO: Show device unreachable dialog
-                    // TODO: If unreachable dialog is already shown, set .retrying to false (with root scope)
-                    // this.showDeviceUnreachableDialog();
-                } else {
-                    this.failSession();
-                }
-            });
+            // Start and handle success/error
+            this.pushPromise = this.pushSession.start()
+                .then(() => this.resetPushSession(true))
+                .catch((error) => {
+                    // Reset push session
+                    this.resetPushSession(false);
+
+                    // Handle error
+                    if (error instanceof TimeoutError) {
+                        // Show device unreachable dialog (if we were already
+                        // connected and if not already visible).
+                        if (this.$state.includes('messenger') && this.deviceUnreachableDialog === null) {
+                            this.deviceUnreachableDialog = this.$mdDialog.show({
+                                controller: DeviceUnreachableController,
+                                controllerAs: 'ctrl',
+                                templateUrl: 'partials/dialog.device-unreachable.html',
+                                parent: angular.element(document.body),
+                                escapeToClose: false,
+                            }).finally(() => this.deviceUnreachableDialog = null);
+                        }
+                    } else {
+                        this.failSession();
+                    }
+                });
 
 
             // Update state
             // Update state
             if (!this.$rootScope.$$phase) {
             if (!this.$rootScope.$$phase) {
@@ -1061,6 +1074,24 @@ export class WebClientService {
         return [this.pushExpectedPeriodMaxMs, this.pushPromise];
         return [this.pushExpectedPeriodMaxMs, this.pushPromise];
     }
     }
 
 
+    /**
+     * Reset push session (if any) and hide the *device unreachable* dialog
+     * (if any and if requested).
+     */
+    private resetPushSession(hideDeviceUnreachableDialog: boolean = true): void {
+        // Hide unreachable dialog (if any)
+        if (hideDeviceUnreachableDialog && this.deviceUnreachableDialog !== null) {
+            this.$mdDialog.hide();
+        }
+
+        // Reset push session (if any)
+        if (this.pushSession !== null) {
+            this.pushSession.done();
+            this.pushSession = null;
+            this.pushPromise = null;
+        }
+    }
+
     /**
     /**
      * Start the webclient service.
      * Start the webclient service.
      * Return a promise that resolves once connected.
      * Return a promise that resolves once connected.
@@ -1121,10 +1152,7 @@ export class WebClientService {
         let remove = false;
         let remove = false;
 
 
         // Stop push session
         // Stop push session
-        if (this.pushSession !== null) {
-            this.pushSession.done();
-            this.pushSession = null;
-        }
+        this.resetPushSession(true);
 
 
         // Session deleted: Force close and delete
         // Session deleted: Force close and delete
         if (args.reason === DisconnectReason.SessionDeleted) {
         if (args.reason === DisconnectReason.SessionDeleted) {