Explorar o código

Implement notification settings (#155)

Fixes #8
IndianaDschones %!s(int64=8) %!d(string=hai) anos
pai
achega
2f1bd536a2

+ 10 - 2
public/i18n/de.json

@@ -161,7 +161,10 @@
         "FILE_MESSAGES_NOT_SUPPORTED": "«{receiverName}» kann noch keine Dateien erhalten.",
         "ERROR_OCCURRED": "Es ist ein Fehler aufgetreten.",
         "FILE_TOO_LARGE": "Aktuell können keine Dateien grösser als 15 MiB über Threema Web versendet werden",
-        "TEXT_TOO_LONG": "Diese Nachricht ist zu lang und kann nicht gesendet werden (Maximale Länge {max} Zeichen)."
+        "TEXT_TOO_LONG": "Diese Nachricht ist zu lang und kann nicht gesendet werden (Maximale Länge {max} Zeichen).",
+        "NOTIFICATION_PERMISSION_DENIED" : "Berechtigung nicht erteilt. Sie müssen die Berechtigungen für Threema Web manuell erteilen.",
+        "NOTIFICATION_PERMISSION_DENIED_LEARN_MORE" : "Mehr erfahren.",
+        "NOTIFICATION_API_NOT_AVAILABLE" : "Ihr Browser unterstützt keine Desktopbenachrichtigungen."
     },
     "mimeTypes": {
         "android_apk": "Android-Paket",
@@ -191,6 +194,11 @@
         "LICENSE_LINK_AFTER": "gefunden werden"
     },
     "settings": {
-        "SETTINGS":"Einstellungen"
+        "SETTINGS":"Einstellungen",
+        "notifications": {
+            "NOTIFICATIONS": "Benachrichtigungen",
+            "SHOW_NOTIFICATIONS": "Desktopbenachrichtigungen anzeigen",
+            "SHOW_PREVIEW": "Nachrichteninhalt in Benachrichtigungen anzeigen"
+        }
     }
 }

+ 10 - 2
public/i18n/en.json

@@ -162,7 +162,10 @@
         "FILE_MESSAGES_NOT_SUPPORTED": "«{receiverName}» cannot receive files.",
         "ERROR_OCCURRED": "An error occurred.",
         "FILE_TOO_LARGE": "Currently files larger than 15 MiB cannot be sent through Threema Web",
-        "TEXT_TOO_LONG": "This message is too long and cannot be sent (Max length {max} characters)."
+        "TEXT_TOO_LONG": "This message is too long and cannot be sent (Max length {max} characters).",
+        "NOTIFICATION_PERMISSION_DENIED" : "Permission denied. You have to grant the permission for Threema Web manually.",
+        "NOTIFICATION_PERMISSION_DENIED_LEARN_MORE" : "Learn more.",
+        "NOTIFICATION_API_NOT_AVAILABLE" : "Your browser does not support desktop notifications."
     },
     "mimeTypes": {
         "android_apk": "Android package",
@@ -192,6 +195,11 @@
         "LICENSE_LINK_AFTER": ""
     },
     "settings": {
-        "SETTINGS":"Settings"
+        "SETTINGS":"Settings",
+        "notifications": {
+            "NOTIFICATIONS" : "Notifications",
+            "SHOW_NOTIFICATIONS" : "Show desktop notifications",
+            "SHOW_PREVIEW" : "Show message contents in notifications"
+        }
     }
 }

+ 41 - 0
src/partials/dialog.settings.html

@@ -11,9 +11,50 @@
         </md-toolbar>
         <md-dialog-content>
             <div class="md-dialog-content">
+                <section>
+                    <md-subheader class="md-accent"><span translate>settings.notifications.NOTIFICATIONS</span>
+                    </md-subheader>
+                    <md-list flex ng-if="ctrl.notificationApiAvailable && ctrl.notificationPermission !== false">
+                        <md-list-item>
+                            <md-checkbox
+                                    ng-disabled="!ctrl.notificationApiAvailable || ctrl.notificationPermission === false"
+                                    ng-model="ctrl.desktopNotifications"
+                                    ng-change="ctrl.setWantsNotifications(ctrl.desktopNotifications)"
+                                    aria-label="Show desktop notifications">
+                                <span translate>settings.notifications.SHOW_NOTIFICATIONS</span>
+                            </md-checkbox>
+                        </md-list-item>
+                        <md-list-item ng-if="ctrl.desktopNotifications">
+                            <md-checkbox
+                                    ng-disabled="!ctrl.desktopNotifications"
+                                    ng-model="ctrl.notificationPreview"
+                                    ng-change="ctrl.setWantsPreview(ctrl.notificationPreview)"
+                                    aria-label="Show message preview">
+                                <span translate>settings.notifications.SHOW_PREVIEW</span>
+                            </md-checkbox>
+                        </md-list-item>
+                    </md-list>
 
+                    <div ng-if="!ctrl.notificationApiAvailable" class="status status-no">
+                        <i class="material-icons md-24">error</i>
+                        <span translate>error.NOTIFICATION_API_NOT_AVAILABLE</span>
+                    </div>
+                    <div ng-if="ctrl.notificationPermission === false && ctrl.notificationApiAvailable" class="status status-no">
+                        <i class="material-icons md-24">error</i> <span translate>error.NOTIFICATION_PERMISSION_DENIED</span>
+                        <a href="https://threema.ch/de/faq/web_notifications"
+                           target="_blank" rel="noopener noreferrer">
+                            <span translate>error.NOTIFICATION_PERMISSION_DENIED_LEARN_MORE</span>
+                        </a>
+                    </div>
+                </section>
             </div>
         </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-click="ctrl.cancel()">
+                <span translate>common.OK</span>
+            </md-button>
+        </md-dialog-actions>
     </form>
 </md-dialog>
 

+ 6 - 6
src/partials/messenger.navigation.html

@@ -20,18 +20,18 @@
                     <span translate>common.SESSION_DELETE</span>
                 </md-button>
             </md-menu-item>
-            <md-menu-item>
-                <md-button ng-click="ctrl.about()">
-                    <md-icon aria-label="About" class="material-icons md-24">info</md-icon>
-                    <span translate>messenger.ABOUT</span>
-                </md-button>
-            </md-menu-item>
             <md-menu-item>
                 <md-button ng-click="ctrl.settings()">
                     <md-icon aria-label="Settings" class="material-icons md-24">settings</md-icon>
                     <span translate>messenger.SETTINGS</span>
                 </md-button>
             </md-menu-item>
+            <md-menu-item>
+                <md-button ng-click="ctrl.about()">
+                    <md-icon aria-label="About" class="material-icons md-24">info</md-icon>
+                    <span translate>messenger.ABOUT</span>
+                </md-button>
+            </md-menu-item>
         </md-menu-content>
     </md-menu>
 </div>

+ 22 - 2
src/partials/messenger.ts

@@ -84,20 +84,32 @@ class SendFileController extends DialogController {
  */
 class SettingsController {
 
-    public static $inject = ['$mdDialog', '$window', 'SettingsService'];
+    public static $inject = ['$mdDialog', '$window', 'SettingsService', 'NotificationService'];
 
     public $mdDialog: ng.material.IDialogService;
     public $window: ng.IWindowService;
     public settingsService: threema.SettingsService;
+    private notificationService: threema.NotificationService;
     public activeElement: HTMLElement | null;
 
+    private desktopNotifications: boolean;
+    private notificationApiAvailable: boolean;
+    private notificationPermission: boolean;
+    private notificationPreview: boolean;
+
     constructor($mdDialog: ng.material.IDialogService,
                 $window: ng.IWindowService,
-                settingsService: threema.SettingsService) {
+                settingsService: threema.SettingsService,
+                notificationService: threema.NotificationService) {
         this.$mdDialog = $mdDialog;
         this.$window = $window;
         this.settingsService = settingsService;
+        this.notificationService = notificationService;
         this.activeElement = document.activeElement as HTMLElement;
+        this.desktopNotifications = notificationService.getWantsNotifications();
+        this.notificationApiAvailable = notificationService.isNotificationApiAvailable();
+        this.notificationPermission = notificationService.getNotificationPermission();
+        this.notificationPreview = notificationService.getWantsPreview();
     }
 
     public cancel(): void {
@@ -117,6 +129,14 @@ class SettingsController {
         }
     }
 
+    public setWantsNotifications(desktopNotifications: boolean) {
+        this.notificationService.setWantsNotifications(desktopNotifications);
+    }
+
+    public setWantsPreview(notificationPreview: boolean) {
+        this.notificationService.setWantsPreview(notificationPreview);
+    }
+
 }
 
 class ConversationController {

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

@@ -324,3 +324,27 @@ md-toast.md-center {
     left: 50%;
     transform: translate3d(-50%, 0, 0);
 }
+
+.md-subheader {
+    background-color: transparent;
+}
+
+.md-subheader-inner {
+    padding-left: 0;
+}
+
+md-list-item .md-list-item-inner > md-checkbox .md-label {
+    display: inline-block;
+    white-space: inherit;
+}
+
+.status span {
+    display: inline;
+    line-height: 24px;
+    vertical-align: top;
+}
+
+.status-yes i { color: #4caf50; }
+.status-no i { color: #f44336; }
+.status-unknown i { color: #0277BD; }
+.small { font-size: 0.8em; font-weight: 300; }

+ 198 - 32
src/services/notification.ts

@@ -16,61 +16,218 @@
  */
 
 import Receiver = threema.Receiver;
+import SettingsService = threema.SettingsService;
 export class NotificationService implements threema.NotificationService {
 
-    private logTag: string = '[NotificationService]';
+    private static SETTINGS_NOTIFICATIONS = 'notifications';
+    private static SETTINGS_NOTIFICATION_PREVIEW = 'notificationPreview';
 
     private $log: ng.ILogService;
     private $window: ng.IWindowService;
     private $state: ng.ui.IStateService;
 
+    private settingsService: SettingsService;
+    private logTag = '[NotificationService]';
+
     // Whether user has granted notification permission
-    private mayNotify: boolean = null;
+    private notificationPermission: boolean = null;
+    // Whether user wants to receive desktop notifications
+    private desktopNotifications: boolean = null;
+    // Whether the browser supports notifications
+    private notificationAPIAvailable: boolean = null;
+    // Whether the user wants notification preview
+    private notificationPreview: boolean = null;
 
     // Cache notifications
     private notificationCache: any = {};
 
-    public static $inject = ['$log', '$window', '$state'];
-    constructor($log: ng.ILogService, $window: ng.IWindowService, $state: ng.ui.IStateService) {
+    public static $inject = ['$log', '$window', '$state', 'SettingsService'];
+
+    constructor($log: ng.ILogService, $window: ng.IWindowService,
+                $state: ng.ui.IStateService, settingsService: SettingsService) {
         this.$log = $log;
         this.$window = $window;
         this.$state = $state;
+        this.settingsService = settingsService;
+    }
+
+    public init(): void {
+        this.checkNotificationAPI();
+        this.fetchSettings();
     }
 
     /**
      * Ask user for desktop notification permissions.
      *
-     * Updates internal `maxNotify` flag. If the user accepts, the is set to to
-     * true. If the user declines, the flag is set to false. If the
-     * Notifications API is not available, the flag is set to null.
+     * Possible values for 'notificationPermission'
+     *      - denied: User has denied the notification permission
+     *      - granted: User has granted the notification permission
+     *      - default: User has visits Threema Web the first time.
+     *                 It stays default unless he denies/grants us the
+     *                 notification permission or if the result is sth. else
+     * If the user grants the permission, the 'desktopNotification' flag
+     * becomes true and is stored in the local storage.
      */
-    public requestNotificationPermission(): void {
-        if (!('Notification' in this.$window)) {
-            // API not available
-            this.$log.warn(this.logTag, 'Notification API not available');
-            this.mayNotify = null;
-        } else {
+    private requestNotificationPermission(): void {
+        if (this.notificationAPIAvailable) {
             const Notification = this.$window.Notification;
-            if (Notification.permission === 'granted') {
-                // Already granted
-                this.$log.debug(this.logTag, 'Notification permission granted');
-                this.mayNotify = true;
-            } else if (Notification.permission === 'denied') {
-                // Not granted
-                this.$log.warn(this.logTag, 'Notification permission denied');
-                this.mayNotify = false;
-            } else {
-                // Ask user
-                this.$log.debug(this.logTag, 'Requesting notification permission');
-                Notification.requestPermission((result) => {
-                    if (result === 'granted') {
-                        this.mayNotify = true;
-                    } else {
-                        this.mayNotify = false;
-                    }
-                });
+            this.$log.debug(this.logTag, 'Requesting notification permission...');
+            Notification.requestPermission((result) => {
+                switch (result) {
+                    case 'denied':
+                        this.notificationPermission = false;
+                        break;
+                    case 'granted':
+                        this.notificationPermission = true;
+                        this.desktopNotifications = true;
+                        this.storeSetting(NotificationService.SETTINGS_NOTIFICATIONS, 'true');
+                        break;
+                    case 'default':
+                        this.desktopNotifications = false;
+                        this.notificationPermission = null;
+                        break;
+                    default:
+                        this.notificationPermission = false;
+                        break;
+                }
+                this.$log.debug(this.logTag, 'Notification permission', this.notificationPermission);
+            });
+        }
+    }
+
+    /**
+     * Check the notification api availability and permission state
+     *
+     * If the api is available, 'notificationAPIAvailable' becomes true and
+     * the permission state is checked
+     */
+    private checkNotificationAPI(): void {
+        this.notificationAPIAvailable = ('Notification' in this.$window);
+        this.$log.debug(this.logTag, 'Notification API available:', this.notificationAPIAvailable);
+        if (this.notificationAPIAvailable) {
+            const Notification = this.$window.Notification;
+            switch (Notification.permission) {
+                // denied means the user must manually re-grant permission via browser settings
+                case 'denied':
+                    this.notificationPermission = false;
+                    break;
+                case 'granted':
+                    this.notificationPermission = true;
+                    break;
+                case 'default':
+                    this.notificationPermission = null;
+                    break;
+                default:
+                    this.notificationPermission = false;
+                    break;
             }
         }
+        this.$log.debug(this.logTag, 'Initial notificationPermission', this.notificationPermission);
+    }
+
+    /**
+     * Get the initial settings from local storage
+     */
+    private fetchSettings(): void {
+        this.$log.debug(this.logTag, 'Fetching settings...');
+        let notifications = this.retrieveSetting(NotificationService.SETTINGS_NOTIFICATIONS);
+        let preview = this.retrieveSetting(NotificationService.SETTINGS_NOTIFICATION_PREVIEW);
+        if (notifications === 'true') {
+            this.$log.debug(this.logTag, 'Desktop notifications:', notifications);
+            this.desktopNotifications = true;
+            // check permission because user may have revoked them
+            this.requestNotificationPermission();
+        } else if (notifications === 'false') {
+            this.$log.debug(this.logTag, 'Desktop notifications:', notifications);
+            // user does not want notifications
+            this.desktopNotifications = false;
+        } else {
+            this.$log.debug(this.logTag, 'Desktop notifications:', notifications, 'Asking user...');
+            // Neither true nor false was in local storage, so we have to ask the user if he wants notifications
+            // If he grants (or already has granted) us the permission, we will set the flag true (default setting)
+            this.requestNotificationPermission();
+        }
+        if (preview === 'false') {
+            this.notificationPreview = false;
+        } else {
+            // set the flag true if true/nothing or sth. else is in local storage (default setting)
+            this.notificationPreview = true;
+            this.storeSetting(NotificationService.SETTINGS_NOTIFICATION_PREVIEW, 'true');
+        }
+    }
+
+    /**
+     * Returns if the user has granted the notification permission
+     * @returns {boolean}
+     */
+    public getNotificationPermission(): boolean {
+        return this.notificationPermission;
+    }
+
+    /**
+     * Returns if the user wants to receive notifications
+     * @returns {boolean}
+     */
+    public getWantsNotifications(): boolean {
+        return this.desktopNotifications;
+    }
+
+    /**
+     * Returns if the user wants a message preview in the notification
+     * @returns {boolean}
+     */
+    public getWantsPreview(): boolean {
+        return this.notificationPreview;
+    }
+
+    /**
+     * Returns if the notification api is available
+     * @returns {boolean}
+     */
+    public isNotificationApiAvailable(): boolean {
+        return this.notificationAPIAvailable;
+    }
+
+    /**
+     * Sets if the user wants desktop notifications
+     * @param wantsNotifications
+     */
+    public setWantsNotifications(wantsNotifications: boolean): void {
+        this.$log.debug(this.logTag, 'Requesting notification preference change to', wantsNotifications);
+        if (wantsNotifications) {
+            this.requestNotificationPermission();
+        } else {
+            this.desktopNotifications = false;
+            this.storeSetting(NotificationService.SETTINGS_NOTIFICATIONS, 'false');
+        }
+    }
+
+    /**
+     * Sets if the user wants a message preview
+     * @param wantsPreview
+     */
+    public setWantsPreview(wantsPreview: boolean): void {
+        this.$log.debug(this.logTag, 'Requesting preview preference change to', wantsPreview);
+        this.notificationPreview = wantsPreview;
+        this.storeSetting(NotificationService.SETTINGS_NOTIFICATION_PREVIEW, wantsPreview.toString());
+    }
+
+    /**
+     * Stores the given key/value pair in local storage
+     * @param key
+     * @param value
+     */
+    private storeSetting(key: string, value: string): void {
+        this.settingsService.storeUntrustedKeyValuePair(key, value);
+    }
+
+    /**
+     * Retrieves the value for the given key from local storage
+     * @param key
+     * @returns {string}
+     */
+    private retrieveSetting(key: string): string {
+        return this.settingsService.retrieveUntrustedKeyValuePair(key);
     }
 
     /**
@@ -87,10 +244,19 @@ export class NotificationService implements threema.NotificationService {
     public showNotification(tag: string, title: string, body: string,
                             avatar: string = '/img/threema-64x64.png', clickCallback: any): boolean {
         // Only show notifications if user granted permission to do so
-        if (this.mayNotify !== true) {
+        if (this.notificationPermission !== true || this.desktopNotifications !== true) {
             return false;
         }
 
+        // Clear body string if the user does not want a notification preview
+        if (!this.notificationPreview) {
+            body = '';
+            // Clear notification cache
+            if (this.notificationCache[tag]) {
+                this.clearCache(tag);
+            }
+        }
+
         // If the cache is not empty, append old messages
         if (this.notificationCache[tag]) {
             body += '\n' + this.notificationCache[tag].body;

+ 3 - 3
src/services/webclient.ts

@@ -406,9 +406,9 @@ export class WebClientService implements threema.WebClientService {
         this.salty.on('handover', () => {
             this.$log.debug('Handover done');
 
-            // Ask user for notification permission
-            this.$log.debug('Check notification permission...');
-            this.notificationService.requestNotificationPermission();
+            // Initialize NotificationService
+            this.$log.debug('Initializing NotificationService...');
+            this.notificationService.init();
 
             // Create secure data channel
             this.$log.debug('Create SecureDataChannel "' + WebClientService.DC_LABEL + '"...');

+ 7 - 1
src/threema.d.ts

@@ -343,7 +343,13 @@ declare namespace threema {
      * Notification service.
      */
     interface NotificationService {
-        requestNotificationPermission(): void;
+        getNotificationPermission(): boolean;
+        getWantsNotifications(): boolean;
+        setWantsNotifications(wantsNotifications: boolean): void;
+        setWantsPreview(wantsPreview: boolean): void;
+        getWantsPreview(): boolean;
+        init(): void;
+        isNotificationApiAvailable(): boolean;
         showNotification(id: string, title: string, body: string,
                          avatar: string | null, clickCallback: any | null): boolean;
         clearCache(tag: string): void;