Sfoglia il codice sorgente

Minimum delay futures (#629)

Replace ExecuteService with an extension to the Future class
Lennart Grahl 6 anni fa
parent
commit
5d76b5e978
4 ha cambiato i file con 57 aggiunte e 91 eliminazioni
  1. 25 0
      public/libs/future.js
  2. 31 29
      src/partials/messenger.ts
  3. 0 62
      src/services/execute.ts
  4. 1 0
      src/types/future.d.ts

+ 25 - 0
public/libs/future.js

@@ -20,6 +20,31 @@ class Future extends Promise {
         this._reject = reject;
     }
 
+    /**
+     * Wrap a promise to ensure it does not resolve before a minimum
+     * duration.
+     *
+     * Note: The promise will still reject immediately. Furthermore, be
+     *       aware that the promise does not resolve/reject inside of
+     *       an AngularJS digest cycle.
+     *
+     * @param promise the promise or future to be wrapped
+     * @param minDuration the minimum duration before it should be resolved
+     * @returns {Future}
+     */
+    static withMinDuration(promise, minDuration) {
+        const start = new Date();
+        return new Future((resolve, reject) => {
+            promise
+                .then((result) => {
+                    const timediff = new Date() - start;
+                    const delay = Math.max(minDuration - timediff, 0);
+                    self.setTimeout(() => resolve(result), delay);
+                })
+                .catch((error) => reject(error));
+        });
+    }
+
     /**
      * Return whether the future is done (resolved or rejected).
      */

+ 31 - 29
src/partials/messenger.ts

@@ -28,7 +28,6 @@ import {bufferToUrl, hasValue, logAdapter, supportsPassive, throttle, u8aToHex}
 import {ContactService} from '../services/contact';
 import {ControllerService} from '../services/controller';
 import {ControllerModelService} from '../services/controller_model';
-import {ExecuteService} from '../services/execute';
 import {FingerPrintService} from '../services/fingerprint';
 import {TrustedKeyStoreService} from '../services/keystore';
 import {MimeService} from '../services/mime';
@@ -1423,25 +1422,26 @@ class ReceiverEditController {
     private logTag: string = '[ReceiverEditController]';
 
     public $mdDialog: any;
+    private $scope: ng.IScope;
     public $state: UiStateService;
     private $translate: ng.translate.ITranslateService;
 
     public title: string;
     private $timeout: ng.ITimeoutService;
-    private execute: ExecuteService;
-    public loading = false;
+    private future: Future<threema.Receiver>;
 
     private controllerModel: threema.ControllerModel<threema.Receiver>;
     public type: string;
 
     public static $inject = [
-        '$log', '$stateParams', '$state', '$mdDialog',
+        '$log', '$scope', '$stateParams', '$state', '$mdDialog',
         '$timeout', '$translate', 'WebClientService', 'ControllerModelService',
     ];
-    constructor($log: ng.ILogService, $stateParams, $state: UiStateService,
+    constructor($log: ng.ILogService, $scope: ng.IScope, $stateParams, $state: UiStateService,
                 $mdDialog, $timeout: ng.ITimeoutService, $translate: ng.translate.ITranslateService,
                 webClientService: WebClientService, controllerModelService: ControllerModelService) {
 
+        this.$scope = $scope;
         this.$mdDialog = $mdDialog;
         this.$state = $state;
         this.$timeout = $timeout;
@@ -1480,8 +1480,6 @@ class ReceiverEditController {
                 return;
         }
         this.type = receiver.type;
-
-        this.execute = new ExecuteService($log, $timeout, 1000);
     }
 
     public keypress($event: KeyboardEvent): void {
@@ -1491,22 +1489,22 @@ class ReceiverEditController {
     }
 
     public save(): void {
-        // show loading
-        this.loading = true;
-
-        // validate first
-        this.execute.execute(this.controllerModel.save())
-            .then((receiver: threema.Receiver) => {
-                this.goBack();
+        this.future = Future.withMinDuration(this.controllerModel.save(), 100);
+        this.future
+            .then(() => {
+                this.$scope.$apply(() => {
+                    this.goBack();
+                });
             })
             .catch((errorCode) => {
-                this.showEditError(errorCode);
+                this.$scope.$apply(() => {
+                    this.showEditError(errorCode);
+                });
             });
     }
 
     public isSaving(): boolean {
-        return this.execute !== undefined
-            && this.execute.isRunning();
+        return this.future !== undefined && !this.future.done;
     }
 
     private showEditError(errorCode: string): void {
@@ -1540,7 +1538,7 @@ class ReceiverCreateController {
     private logTag: string = '[ReceiverEditController]';
 
     public $mdDialog: any;
-    private loading = false;
+    private $scope: ng.IScope;
     private $timeout: ng.ITimeoutService;
     private $log: ng.ILogService;
     private $state: UiStateService;
@@ -1548,16 +1546,17 @@ class ReceiverCreateController {
     public identity = '';
     private $translate: any;
     public type: string;
-    private execute: ExecuteService;
+    private future: Future<threema.Receiver>;
 
     public controllerModel: threema.ControllerModel<threema.Receiver>;
 
-    public static $inject = ['$stateParams', '$mdDialog', '$mdToast', '$translate',
+    public static $inject = ['$stateParams', '$mdDialog', '$scope', '$mdToast', '$translate',
         '$timeout', '$state', '$log', 'ControllerModelService'];
-    constructor($stateParams: CreateReceiverStateParams, $mdDialog, $mdToast, $translate,
+    constructor($stateParams: CreateReceiverStateParams, $mdDialog, $scope: ng.IScope, $mdToast, $translate,
                 $timeout: ng.ITimeoutService, $state: UiStateService, $log: ng.ILogService,
                 controllerModelService: ControllerModelService) {
         this.$mdDialog = $mdDialog;
+        this.$scope = $scope;
         this.$timeout = $timeout;
         this.$state = $state;
         this.$log = $log;
@@ -1586,11 +1585,10 @@ class ReceiverCreateController {
             default:
                 this.$log.error('invalid type', this.type);
         }
-        this.execute = new ExecuteService($log, $timeout, 1000);
     }
 
     public isSaving(): boolean {
-        return this.execute.isRunning();
+        return this.future !== undefined && !this.future.done;
     }
 
     public goBack(): void {
@@ -1619,15 +1617,19 @@ class ReceiverCreateController {
     }
 
     public create(): void {
-        // Show loading indicator
-        this.loading = true;
-
         // Save, then go to receiver detail page
-        this.execute.execute(this.controllerModel.save())
+        this.future = Future.withMinDuration(this.controllerModel.save(), 100);
+        this.future
             .then((receiver: threema.Receiver) => {
-                this.$state.go('messenger.home.detail', receiver, {location: 'replace'});
+                this.$scope.$apply(() => {
+                    this.$state.go('messenger.home.detail', receiver, {location: 'replace'});
+                });
             })
-            .catch(this.showAddError.bind(this));
+            .catch((errorCode) => {
+                this.$scope.$apply(() => {
+                    this.showAddError(errorCode);
+                });
+            });
     }
 }
 

+ 0 - 62
src/services/execute.ts

@@ -1,62 +0,0 @@
-/**
- * This file is part of Threema Web.
- *
- * Threema Web is free software: you can redistribute it and/or modify it
- * under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or (at
- * your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
- * General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * Execute a promise and wait x (timeout) seconds for end the process
- * Used for a better user experience on saving forms
- */
-export class ExecuteService {
-    private $log: ng.ILogService;
-    private $timeoutService: ng.ITimeoutService;
-
-    private timeout: number;
-    private started = false;
-
-    public static $inject = ['$log', '$timeout'];
-    constructor($log: ng.ILogService, $timeout: ng.ITimeoutService, timeout = 0) {
-        // Angular services
-        this.$log = $log;
-        this.$timeoutService = $timeout;
-        this.timeout = timeout;
-    }
-
-    public execute(runnable: Promise<any>): Promise<any> {
-        if (this.started) {
-            this.$log.error('execute already in progress');
-            return null;
-        }
-
-        this.started = true;
-        return new Promise((a, e) => {
-            runnable
-                .then((arg: any) => {
-                    this.$timeoutService(() => {
-                        this.started = false;
-                        a(arg);
-                    }, this.timeout);
-                })
-                .catch((arg: any) => {
-                    this.started = false;
-                    e(arg);
-                });
-        });
-    }
-
-    public isRunning(): boolean {
-        return this.started;
-    }
-}

+ 1 - 0
src/types/future.d.ts

@@ -40,6 +40,7 @@ interface FutureStatic {
     new<T>(executor?: (resolveFn: (value?: T | PromiseLike<T>) => void,
                        rejectFn: (reason?: any) => void) => void,
     ): Future<T>
+    withMinDuration<T>(promise: Promise<T>, minDuration: Number): Future<T>
 }
 
 declare var Future: FutureStatic;