瀏覽代碼

Upgrade to Angular 1.7

Danilo Bargen 7 年之前
父節點
當前提交
c328952b26

+ 2 - 2
LICENSE-3RD-PARTY.txt

@@ -125,7 +125,7 @@ License for angular-material
 
 The MIT License
 
-Copyright (c) 2014 Google, Inc. http://angularjs.org
+Copyright (c) 2014-2018 Google, Inc. http://angularjs.org
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -241,7 +241,7 @@ License for angular-translate
 
 The MIT License (MIT)
 
-Copyright (c) <2014> <pascal.precht@gmail.com>
+Copyright (c) 2013-2017 The angular-translate team and Pascal Precht
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

+ 2 - 1
dist/package.sh

@@ -63,7 +63,8 @@ targets=(
     @saltyrtc/task-relayed-data/dist/saltyrtc-task-relayed-data.es5.js
     webrtc-adapter/out/adapter_no_edge.js
     webrtc-adapter/out/adapter.js
-    qrcode-generator/js/qrcode.js
+    qrcode-generator/qrcode.js
+    qrcode-generator/qrcode_UTF8.js
     angular-qrcode/angular-qrcode.js
     angularjs-scroll-glue/src/scrollglue.js
     angular-material/angular-material.min.js

+ 2 - 1
index.html

@@ -110,7 +110,8 @@
     <script src="node_modules/babel-es6-polyfill/browser-polyfill.min.js?v=[[VERSION]]"></script>
 
     <!-- Various libraries -->
-    <script src="node_modules/qrcode-generator/js/qrcode.js?v=[[VERSION]]"></script>
+    <script src="node_modules/qrcode-generator/qrcode.js?v=[[VERSION]]"></script>
+    <script src="node_modules/qrcode-generator/qrcode_UTF8.js?v=[[VERSION]]"></script>
     <script src="node_modules/angular-qrcode/angular-qrcode.js?v=[[VERSION]]"></script>
     <script src="node_modules/angular-material/angular-material.min.js?v=[[VERSION]]"></script>
     <script src="node_modules/angular-ui-router/release/angular-ui-router.min.js?v=[[VERSION]]"></script>

文件差異過大導致無法顯示
+ 459 - 200
package-lock.json


+ 18 - 17
package.json

@@ -29,23 +29,24 @@
     "@saltyrtc/client": "^0.11.3",
     "@saltyrtc/task-relayed-data": "^0.2.0",
     "@saltyrtc/task-webrtc": "^0.11.0",
-    "@types/angular": "^1.6.45",
+    "@types/angular": "^1.6.48",
     "@types/angular-material": "^1.1.59",
     "@types/angular-sanitize": "^1.3.7",
-    "@types/angular-translate": "~2.15.4",
+    "@types/angular-translate": "^2.16.0",
     "@types/angular-ui-router": "^1.1.40",
     "@types/filesaver": "~0.0.30",
-    "@types/jquery": "^3.3.2",
+    "@types/jquery": "^3.3.4",
     "@types/msgpack-lite": "^0.1.6",
     "@types/webrtc": "0.0.23",
-    "angular": "~1.5.10",
-    "angular-animate": "~1.5.10",
-    "angular-aria": "~1.5.10",
-    "angular-material": "=1.1.1",
-    "angular-qrcode": "~6.2.1",
-    "angular-route": "~1.5.10",
-    "angular-sanitize": "~1.5.10",
-    "angular-translate": "~2.13.1",
+    "angular": "~1.7.2",
+    "angular-animate": "~1.7.2",
+    "angular-aria": "~1.7.2",
+    "angular-material": "=1.1.10",
+    "angular-messages": "^1.7.2",
+    "angular-qrcode": "~7.2",
+    "angular-route": "~1.7.2",
+    "angular-sanitize": "~1.7.2",
+    "angular-translate": "~2.18",
     "angular-ui-router": "~0.3.2",
     "angularjs-scroll-glue": "~2.1.0",
     "autolinker": "~0.27.0",
@@ -59,25 +60,25 @@
     "js-sha256": "~0.3.2",
     "messageformat": "~1.0.2",
     "msgpack-lite": "~0.1.26",
-    "node-sass": "^4.9.0",
+    "node-sass": "^4.9.2",
     "sdp": "~1.3.0",
     "ts-events": "^3.1.5",
     "tsify": "~4.0.0",
     "tweetnacl": "^1.0.0",
-    "typescript": "~2.9",
+    "typescript": "^2.9.2",
     "webrtc-adapter": "~3.4.3"
   },
   "devDependencies": {
     "@types/jasmine": "^2.8.8",
-    "angular-mocks": "~1.5.10",
+    "angular-mocks": "~1.7",
     "budo": "^9",
     "concurrently": "~3.3.0",
     "jasmine": "^3.1.0",
-    "jasmine-core": "~2.5.2",
-    "karma": "^2",
+    "jasmine-core": "^3.1.0",
+    "karma": "^2.0.4",
     "karma-chrome-launcher": "^2.2.0",
     "karma-firefox-launcher": "^1.1.0",
     "karma-jasmine": "^1.1.2",
-    "tslint": "~5.9"
+    "tslint": "~5.10"
   }
 }

+ 118 - 107
src/directives/avatar.ts

@@ -40,41 +40,7 @@ export default [
             controller: [function() {
                 this.logTag = '[Directives.Avatar]';
 
-                this.highResolution = this.resolution === 'high';
-                this.isLoading = this.highResolution;
-                this.backgroundColor = this.receiver.color;
                 let loadingPromise: ng.IPromise<any> = null;
-                this.avatarClass = () => {
-                    return 'avatar-' + this.resolution + (this.isLoading ? ' is-loading' : '');
-                };
-
-                this.avatarExists = () => {
-                    if (this.receiver.avatar === undefined
-                        || this.receiver.avatar[this.resolution] === undefined
-                        || this.receiver.avatar[this.resolution] === null) {
-                        return false;
-                    }
-                    this.isLoading = false;
-                    // Reset background color
-                    this.backgroundColor = null;
-                    return true;
-                };
-
-                /**
-                 * Return path to the default avatar.
-                 */
-                this.getDefaultAvatarUri = (type: threema.ReceiverType, highResolution: boolean) => {
-                    switch (type) {
-                        case 'group':
-                            return highResolution ? 'img/ic_group_picture_big.png' : 'img/ic_group_t.png';
-                        case 'distributionList':
-                            return highResolution ? 'img/ic_distribution_list_t.png' : 'img/ic_distribution_list_t.png';
-                        case 'contact':
-                        case 'me':
-                        default:
-                            return highResolution ? 'img/ic_contact_picture_big.png' : 'img/ic_contact_picture_t.png';
-                    }
-                };
 
                 /**
                  * Convert avatar bytes to an URI.
@@ -98,86 +64,131 @@ export default [
                     return avatarUri[res];
                 };
 
-                /**
-                 * Return an avatar URI.
-                 *
-                 * This will fall back to a low resolution version or to the
-                 * default avatar if no avatar for the desired resolution could
-                 * be found.
-                 */
-                this.getAvatarUri = () => {
-                    /// If an avatar for the chosen resolution exists, convert it to an URI and return
-                    if (this.avatarExists()) {
-                        return this.avatarToUri(this.receiver.avatar[this.resolution], this.resolution);
-                    }
+                this.$onInit = function() {
 
-                    // Otherwise, if we requested a high res avatar but
-                    // there is only a low-res version, show that.
-                    if (this.highResolution
-                        && this.receiver.avatar !== undefined
-                        && this.receiver.avatar.low !== undefined
-                        && this.receiver.avatar.low !== null) {
-                        return this.avatarToUri(this.receiver.avatar.low, 'low');
-                    }
+                    this.highResolution = this.resolution === 'high';
+                    this.isLoading = this.highResolution;
+                    this.backgroundColor = this.receiver.color;
+                    this.avatarClass = () => {
+                        return 'avatar-' + this.resolution + (this.isLoading ? ' is-loading' : '');
+                    };
 
-                    // As a fallback, get the default avatar.
-                    return this.getDefaultAvatarUri(this.receiver.type, this.highResolution);
-                };
+                    this.avatarExists = () => {
+                        if (this.receiver.avatar === undefined
+                            || this.receiver.avatar[this.resolution] === undefined
+                            || this.receiver.avatar[this.resolution] === null) {
+                            return false;
+                        }
+                        this.isLoading = false;
+                        // Reset background color
+                        this.backgroundColor = null;
+                        return true;
+                    };
 
-                this.requestAvatar = (inView: boolean) => {
-                    if (this.avatarExists()) {
-                        // do not request
-                        return;
-                    }
+                    /**
+                     * Return path to the default avatar.
+                     */
+                    this.getDefaultAvatarUri = (type: threema.ReceiverType, highResolution: boolean) => {
+                        switch (type) {
+                            case 'group':
+                                return highResolution
+                                    ? 'img/ic_group_picture_big.png'
+                                    : 'img/ic_group_t.png';
+                            case 'distributionList':
+                                return highResolution
+                                    ? 'img/ic_distribution_list_t.png'
+                                    : 'img/ic_distribution_list_t.png';
+                            case 'contact':
+                            case 'me':
+                            default:
+                                return highResolution
+                                    ? 'img/ic_contact_picture_big.png'
+                                    : 'img/ic_contact_picture_t.png';
+                        }
+                    };
 
-                    if (inView) {
-                        if (loadingPromise === null) {
-                            // Do not wait on high resolution avatar
-                            const loadingTimeout = this.highResolution ? 0 : 500;
-                            loadingPromise = $timeout(() => {
-                                // show loading only on high res images!
-                                webClientService.requestAvatar({
-                                    type: this.receiver.type,
-                                    id: this.receiver.id,
-                                } as threema.Receiver, this.highResolution).then((avatar) => {
-                                    $rootScope.$apply(() => {
-                                        this.isLoading = false;
-                                    });
-                                }).catch(() => {
-                                    $rootScope.$apply(() => {
-                                        this.isLoading = false;
+                    /**
+                     * Return an avatar URI.
+                     *
+                     * This will fall back to a low resolution version or to the
+                     * default avatar if no avatar for the desired resolution could
+                     * be found.
+                     */
+                    this.getAvatarUri = () => {
+                        /// If an avatar for the chosen resolution exists, convert it to an URI and return
+                        if (this.avatarExists()) {
+                            return this.avatarToUri(this.receiver.avatar[this.resolution], this.resolution);
+                        }
+
+                        // Otherwise, if we requested a high res avatar but
+                        // there is only a low-res version, show that.
+                        if (this.highResolution
+                            && this.receiver.avatar !== undefined
+                            && this.receiver.avatar.low !== undefined
+                            && this.receiver.avatar.low !== null) {
+                            return this.avatarToUri(this.receiver.avatar.low, 'low');
+                        }
+
+                        // As a fallback, get the default avatar.
+                        return this.getDefaultAvatarUri(this.receiver.type, this.highResolution);
+                    };
+
+                    this.requestAvatar = (inView: boolean) => {
+                        if (this.avatarExists()) {
+                            // do not request
+                            return;
+                        }
+
+                        if (inView) {
+                            if (loadingPromise === null) {
+                                // Do not wait on high resolution avatar
+                                const loadingTimeout = this.highResolution ? 0 : 500;
+                                loadingPromise = $timeout(() => {
+                                    // show loading only on high res images!
+                                    webClientService.requestAvatar({
+                                        type: this.receiver.type,
+                                        id: this.receiver.id,
+                                    } as threema.Receiver, this.highResolution).then((avatar) => {
+                                        $rootScope.$apply(() => {
+                                            this.isLoading = false;
+                                        });
+                                    }).catch(() => {
+                                        $rootScope.$apply(() => {
+                                            this.isLoading = false;
+                                        });
                                     });
-                                });
-                            }, loadingTimeout);
+                                }, loadingTimeout);
+                            }
+                        } else if (loadingPromise !== null) {
+                            // Cancel pending avatar loading
+                            $timeout.cancel(loadingPromise);
+                            loadingPromise = null;
                         }
-                    } else if (loadingPromise !== null) {
-                        // Cancel pending avatar loading
-                        $timeout.cancel(loadingPromise);
-                        loadingPromise = null;
-                    }
-                };
+                    };
+
+                    const isWork = webClientService.clientInfo.isWork;
+                    this.showWorkIndicator = () => {
+                        if (!isContactReceiver(this.receiver)) { return false; }
+                        const contact: threema.ContactReceiver = this.receiver;
+                        return isWork === false
+                            && !this.highResolution
+                            && contact.identityType === threema.IdentityType.Work;
+                    };
+                    this.showHomeIndicator = () => {
+                        if (!isContactReceiver(this.receiver)) { return false; }
+                        const contact: threema.ContactReceiver = this.receiver;
+                        return isWork === true
+                            && !isGatewayContact(contact)
+                            && !isEchoContact(contact)
+                            && contact.identityType === threema.IdentityType.Regular
+                            && !this.highResolution;
+                    };
+                    this.showBlocked = () => {
+                        if (!isContactReceiver(this.receiver)) { return false; }
+                        const contact: threema.ContactReceiver = this.receiver;
+                        return !this.highResolution && contact.isBlocked;
+                    };
 
-                const isWork = webClientService.clientInfo.isWork;
-                this.showWorkIndicator = () => {
-                    if (!isContactReceiver(this.receiver)) { return false; }
-                    const contact: threema.ContactReceiver = this.receiver;
-                    return isWork === false
-                        && !this.highResolution
-                        && contact.identityType === threema.IdentityType.Work;
-                };
-                this.showHomeIndicator = () => {
-                    if (!isContactReceiver(this.receiver)) { return false; }
-                    const contact: threema.ContactReceiver = this.receiver;
-                    return isWork === true
-                        && !isGatewayContact(contact)
-                        && !isEchoContact(contact)
-                        && contact.identityType === threema.IdentityType.Regular
-                        && !this.highResolution;
-                };
-                this.showBlocked = () => {
-                    if (!isContactReceiver(this.receiver)) { return false; }
-                    const contact: threema.ContactReceiver = this.receiver;
-                    return !this.highResolution && contact.isBlocked;
                 };
             }],
             template: `

+ 77 - 76
src/directives/avatar_area.ts

@@ -52,91 +52,92 @@ export default [
                 this.avatar = null; // Type: String
                 this.avatarFormat = webClientService.appCapabilities.imageFormat.avatar;
 
-                this.imageChanged = function(image: ArrayBuffer, notify = true) {
-                    this.isLoading = true;
-                    if (notify === true && this.onChange !== undefined) {
-                        this.onChange(image);
-                    }
-                    this.avatar = image;
-                    this.isLoading = false;
-                };
+                this.$onInit = function() {
+                    this.imageChanged = function(image: ArrayBuffer, notify = true) {
+                        this.isLoading = true;
+                        if (notify === true && this.onChange !== undefined) {
+                            this.onChange(image);
+                        }
+                        this.avatar = image;
+                        this.isLoading = false;
+                    };
 
-                if (this.loadAvatar !== undefined) {
-                    this.isLoading = true;
-                    (this.loadAvatar as Promise<ArrayBuffer>)
-                        .then((image: ArrayBuffer) => {
-                            $rootScope.$apply(() => {
-                                this.avatar = image;
-                                this.isLoading = false;
-                            });
-                        })
-                        .catch(() => {
-                            $rootScope.$apply(() => {
-                                this.isLoading = false;
+                    if (this.loadAvatar !== undefined) {
+                        this.isLoading = true;
+                        (this.loadAvatar as Promise<ArrayBuffer>)
+                            .then((image: ArrayBuffer) => {
+                                $rootScope.$apply(() => {
+                                    this.avatar = image;
+                                    this.isLoading = false;
+                                });
+                            })
+                            .catch(() => {
+                                $rootScope.$apply(() => {
+                                    this.isLoading = false;
+                                });
                             });
-                        });
-                }
+                    }
 
-                this.delete = () => {
-                    this.imageChanged(null, true);
-                };
+                    this.delete = () => {
+                        this.imageChanged(null, true);
+                    };
 
-                // show editor in a dialog
-                this.modify = (ev) => {
-                    $mdDialog.show({
-                        controllerAs: 'ctrl',
-                        controller: function() {
-                            this.avatar = null;
+                    // show editor in a dialog
+                    this.modify = (ev) => {
+                        $mdDialog.show({
+                            controllerAs: 'ctrl',
+                            controller: function() {
+                                this.avatar = null;
 
-                            this.apply = () => {
-                                $mdDialog.hide(this.avatar);
-                            };
+                                this.apply = () => {
+                                    $mdDialog.hide(this.avatar);
+                                };
 
-                            this.cancel = () => {
-                                $mdDialog.cancel();
-                            };
+                                this.cancel = () => {
+                                    $mdDialog.cancel();
+                                };
 
-                            this.changeAvatar = (image: ArrayBuffer) => {
-                                this.avatar = image;
-                            };
-                        },
-                        template: `
-                            <md-dialog translate-attr="{'aria-label': 'messenger.UPLOAD_AVATAR'}">
-                                <form ng-cloak>
-                                 <md-toolbar>
-                                  <div class="md-toolbar-tools">
-                                   <h2 translate>messenger.UPLOAD_AVATAR</h2>
-                                   </div>
-                                   </md-toolbar>
-                                    <md-dialog-content>
-                                        <div class="md-dialog-content avatar-area editor">
-                                            <avatar-editor on-change="ctrl.changeAvatar"></avatar-editor>
-                                        </div>
-                                    </md-dialog-content>
-                                    <md-dialog-actions layout="row" >
-                                          <md-button ng-click="ctrl.cancel()">
-                                           <span translate>common.CANCEL</span>
-                                            </md-button>
-                                      <md-button ng-click="ctrl.apply()">
-                                         <span translate>common.OK</span>
-                                      </md-button>
-                                    </md-dialog-actions>
-                                </form>
-                            </md-dialog>
+                                this.changeAvatar = (image: ArrayBuffer) => {
+                                    this.avatar = image;
+                                };
+                            },
+                            template: `
+                                <md-dialog translate-attr="{'aria-label': 'messenger.UPLOAD_AVATAR'}">
+                                    <form ng-cloak>
+                                     <md-toolbar>
+                                      <div class="md-toolbar-tools">
+                                       <h2 translate>messenger.UPLOAD_AVATAR</h2>
+                                       </div>
+                                       </md-toolbar>
+                                        <md-dialog-content>
+                                            <div class="md-dialog-content avatar-area editor">
+                                                <avatar-editor on-change="ctrl.changeAvatar"></avatar-editor>
+                                            </div>
+                                        </md-dialog-content>
+                                        <md-dialog-actions layout="row" >
+                                              <md-button ng-click="ctrl.cancel()">
+                                               <span translate>common.CANCEL</span>
+                                                </md-button>
+                                          <md-button ng-click="ctrl.apply()">
+                                             <span translate>common.OK</span>
+                                          </md-button>
+                                        </md-dialog-actions>
+                                    </form>
+                                </md-dialog>
 
-                        `,
-                        parent: angular.element(document.body),
-                        targetEvent: ev,
-                        clickOutsideToClose: true,
-                    })
-                        .then((newImage: ArrayBuffer) => {
-                            // update image only if a image was set or if enable clear is true
-                            if (this.enableClear === true || newImage !== null) {
-                                this.imageChanged(newImage, true);
-                            }
-                        }, () => null);
+                            `,
+                            parent: angular.element(document.body),
+                            targetEvent: ev,
+                            clickOutsideToClose: true,
+                        })
+                            .then((newImage: ArrayBuffer) => {
+                                // update image only if a image was set or if enable clear is true
+                                if (this.enableClear === true || newImage !== null) {
+                                    this.imageChanged(newImage, true);
+                                }
+                            }, () => null);
+                    };
                 };
-
             }],
             template: `
                 <div class="avatar-area overview">

+ 1 - 1
src/directives/avatar_editor.ts

@@ -256,7 +256,7 @@ export default [
             template: `
                 <div class="avatar-editor">
                     <div class="avatar-editor-drag croppie-container"></div>
-                    <div class="avatar-editor-navigation"  layout="column" layout-wrap layout-margin layout-align="center center">
+                    <div class="avatar-editor-navigation" layout="column" layout-wrap layout-margin layout-align="center center">
                         <input class="file-input" type="file" style="visibility: hidden" multiple/>
                           <md-button type="submit" class="file-trigger md-raised">
                             <span translate>messenger.UPLOAD_AVATAR</span>

+ 14 - 12
src/directives/contact_badge.ts

@@ -37,20 +37,22 @@ export default [
             },
             controllerAs: 'ctrl',
             controller: [function() {
-                if (this.contactReceiver === undefined) {
-                    this.contactReceiver = webClientService.contacts.get(this.identity);
-                } else {
-                    this.identity = this.contactReceiver.id;
-                }
-
-                this.click = () => {
-                    if (this.linked !== undefined
-                        && this.linked === true) {
-                        $state.go('messenger.home.conversation', {type: 'contact', id: this.identity, initParams: null});
+                this.$onInit = function() {
+                    if (this.contactReceiver === undefined) {
+                        this.contactReceiver = webClientService.contacts.get(this.identity);
+                    } else {
+                        this.identity = this.contactReceiver.id;
                     }
-                };
 
-                this.showActions = this.onRemove !== undefined;
+                    this.click = () => {
+                        if (this.linked !== undefined
+                            && this.linked === true) {
+                            $state.go('messenger.home.conversation', {type: 'contact', id: this.identity, initParams: null});
+                        }
+                    };
+
+                    this.showActions = this.onRemove !== undefined;
+                };
             }],
             template: `
                 <div class="contact-badge receiver-badge" ng-click="ctrl.click()">

+ 0 - 1
src/directives/distribution_list_badge.ts

@@ -26,7 +26,6 @@ export default [
             scope: {},
             bindToController: {
                 distributionListReceiver: '=eeeDistributionListReceiver',
-                contactReceiver: '=?eeeContactReceiver',
             },
             controllerAs: 'ctrl',
             controller: [function() {

+ 15 - 17
src/directives/group_badge.ts

@@ -31,23 +31,6 @@ export default [
             },
             controllerAs: 'ctrl',
             controller: [function() {
-                this.showRoleIcon = this.contactReceiver !== undefined;
-                if (this.showRoleIcon) {
-
-                    if (this.contactReceiver.id === this.groupReceiver.administrator) {
-                        this.roleIcon = 'people';
-                        $translate('messenger.GROUP_ROLE_CREATOR')
-                            .then((label) =>  {
-                                this.roleName = label;
-                            });
-                    } else {
-                        this.roleIcon = 'people_outline';
-                        $translate('messenger.GROUP_ROLE_NORMAL')
-                            .then((label) =>  {
-                                this.roleName = label;
-                            });
-                    }
-                }
                 this.click = () => {
                     $state.go('messenger.home.conversation', {
                         type: 'group',
@@ -55,6 +38,21 @@ export default [
                         initParams: null,
                     });
                 };
+
+                this.$onInit = function() {
+                    this.showRoleIcon = this.contactReceiver !== undefined;
+                    if (this.showRoleIcon) {
+                        if (this.contactReceiver.id === this.groupReceiver.administrator) {
+                            this.roleIcon = 'people';
+                            $translate('messenger.GROUP_ROLE_CREATOR')
+                                .then((label) => this.roleName = label);
+                        } else {
+                            this.roleIcon = 'people_outline';
+                            $translate('messenger.GROUP_ROLE_NORMAL')
+                                .then((label) => this.roleName = label);
+                        }
+                    }
+                };
             }],
             template: `
                 <div class="group-badge receiver-badge" ng-click="ctrl.click()">

+ 57 - 63
src/directives/latest_message.ts

@@ -15,6 +15,7 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
+import {getSenderIdentity} from '../helpers/messages';
 import {MessageService} from '../services/message';
 import {ReceiverService} from '../services/receiver';
 import {WebClientService} from '../services/webclient';
@@ -33,80 +34,73 @@ export default [
             },
             controllerAs: 'ctrl',
             controller: [function() {
-                // Utilities
-                const getIdentity = function(message) {
-                    // TODO: Get rid of duplication with eeeMessage directive
-                    if (message.isOutbox) {
-                        return webClientService.me.id;
-                    }
-                    if (message.partnerId !== null) {
-                        return message.partnerId;
-                    }
-                    return null;
-                };
-                // Conversation properties
+                this.$onInit = function() {
 
-                this.isGroup = this.type as threema.ReceiverType === 'group';
-                this.isDistributionList = !this.isGroup
-                    && this.type as threema.ReceiverType === 'distributionList';
+                    // Conversation properties
+                    this.isGroup = this.type as threema.ReceiverType === 'group';
+                    this.isDistributionList = !this.isGroup
+                        && this.type as threema.ReceiverType === 'distributionList';
 
-                this.showVoipInfo = this.message
-                    && (this.message as threema.Message).type === 'voipStatus';
+                    this.showVoipInfo = this.message
+                        && (this.message as threema.Message).type === 'voipStatus';
 
-                if (this.showVoipInfo) {
-                    this.statusIcon = 'phone_locked';
-                } else if (this.isGroup) {
-                    this.statusIcon = 'group';
-                } else if (this.isDistributionList) {
-                    this.statusIcon = 'forum';
-                } else if (!this.message.isOutbox) {
-                    this.statusIcon = 'reply';
-                } else if (messageService.showStatusIcon(this.message, this.receiver)) {
-                    // Show status icon of incoming messages every time
-                    this.statusIcon = $filter('messageStateIcon')(this.message);
-                } else {
-                    // Do not show a status icon
-                    this.statusIcon = null;
-                }
+                    if (this.showVoipInfo) {
+                        this.statusIcon = 'phone_locked';
+                    } else if (this.isGroup) {
+                        this.statusIcon = 'group';
+                    } else if (this.isDistributionList) {
+                        this.statusIcon = 'forum';
+                    } else if (!this.message.isOutbox) {
+                        this.statusIcon = 'reply';
+                    } else if (messageService.showStatusIcon(this.message, this.receiver)) {
+                        // Show status icon of incoming messages every time
+                        this.statusIcon = $filter('messageStateIcon')(this.message);
+                    } else {
+                        // Do not show a status icon
+                        this.statusIcon = null;
+                    }
 
-                // Find sender of latest message in group chats
-                this.contact = null;
-                if (this.message) {
-                    this.contact = webClientService.contacts.get(getIdentity(this.message));
-                }
+                    // Find sender of latest message
+                    this.contact = null;
+                    if (this.message) {
+                        this.contact = webClientService.contacts.get(
+                            getSenderIdentity(this.message, webClientService.me.id),
+                        );
+                    }
+
+                    // Typing indicator
+                    this.isTyping = () => false;
+                    if (this.isGroup === false
+                        && this.isDistributionList === false
+                        && this.contact !== null) {
+                        this.isTyping = () => {
+                            return webClientService.isTyping(this.contact);
+                        };
+                    }
 
-                // Typing indicator
-                this.isTyping = () => false;
-                if (this.isGroup === false
-                    && this.isDistributionList === false
-                    && this.contact !== null) {
-                    this.isTyping = () => {
-                        return webClientService.isTyping(this.contact);
+                    this.isHidden = () => {
+                        return this.receiver.locked;
                     };
-                }
 
-                this.isHidden = () => {
-                    return this.receiver.locked;
-                };
+                    // Show...
+                    this.showIcon = this.message
+                        && this.message.type !== 'text'
+                        && this.message.type !== 'status';
 
-                // Show...
-                this.showIcon = this.message
-                    && this.message.type !== 'text'
-                    && this.message.type !== 'status';
+                    this.getDraft = () => {
+                        return webClientService.getDraft(this.receiver);
+                    };
 
-                this.getDraft = () => {
-                    return webClientService.getDraft(this.receiver);
-                };
+                    this.showDraft = () => {
+                        if (receiverService.isConversationActive(this.receiver)) {
+                            // Don't show draft if conversation is active
+                            return false;
+                        }
+                        const draft = this.getDraft();
+                        return draft !== undefined && draft !== null;
+                    };
 
-                this.showDraft = () => {
-                    if (receiverService.isConversationActive(this.receiver)) {
-                        // Don't show draft if conversation is active
-                        return false;
-                    }
-                    const draft = this.getDraft();
-                    return draft !== undefined && draft !== null;
                 };
-
             }],
             templateUrl: 'directives/latest_message.html',
         };

+ 2 - 2
src/directives/member_list_editor.ts

@@ -18,6 +18,8 @@
 import {hasFeature} from '../helpers';
 import {WebClientService} from '../services/webclient';
 
+const AUTOCOMPLETE_MAX_RESULTS = 20;
+
 export default [
     '$log', 'WebClientService',
     function($log: ng.ILogService, webClientService: WebClientService) {
@@ -31,8 +33,6 @@ export default [
             },
             controllerAs: 'ctrl',
             controller: [function() {
-                const AUTOCOMPLETE_MAX_RESULTS = 20;
-
                 // Cache all contacts with group chat support
                 this.allContacts = Array
                     .from(webClientService.contacts.values())

+ 150 - 153
src/directives/message.ts

@@ -17,6 +17,7 @@
 
 // tslint:disable:max-line-length
 
+import {getSenderIdentity} from '../helpers/messages';
 import {MessageService} from '../services/message';
 import {ReceiverService} from '../services/receiver';
 import {WebClientService} from '../services/webclient';
@@ -51,168 +52,164 @@ export default [
             controllerAs: 'ctrl',
             controller: [function() {
                 this.logTag = '[MessageDirective]';
-                // Return the contact
-                const getIdentity = function(message: threema.Message) {
-                    if (message.isOutbox) {
-                        return webClientService.me.id;
-                    }
-                    if (message.partnerId != null) {
-                        return message.partnerId;
-                    }
-                    return null;
-                };
-
-                // Defaults and variables
-                if (this.resolution == null) {
-                    this.resolution = 'low';
-                }
 
-                this.contact = webClientService.contacts.get(getIdentity(this.message));
-
-                // Show...
-                this.isStatusMessage = this.message.isStatus;
-                this.isContactMessage = !this.message.isStatus
-                    && webClientService.contacts.has(getIdentity(this.message));
-                this.isGroup = this.type as threema.ReceiverType === 'group';
-                this.isContact = this.type as threema.ReceiverType === 'contact';
-                this.isBusinessReceiver = receiverService.isBusinessContact(this.receiver);
-
-                this.showName = !this.message.isOutbox && this.isGroup;
-                // show avatar only if a name is shown
-                this.showAvatar = this.showName;
-                this.showText = this.message.type === 'text' || this.message.caption;
-                this.showMedia = this.message.type !== 'text';
-                this.showState = messageService.showStatusIcon(this.message as threema.Message, this.receiver);
-                this.showQuote = this.message.quote !== undefined;
-                this.showVoipInfo = this.message.type === 'voipStatus';
-
-                this.access = messageService.getAccess(this.message, this.receiver);
-
-                this.ack = (ack: boolean) => {
-                    webClientService.ackMessage(this.receiver, this.message, ack);
-                };
-
-                this.quote = () => {
-                    // set message as quoted
-                    webClientService.setQuote(this.receiver, this.message);
-                };
-
-                this.delete = (ev) => {
-                    const confirm = $mdDialog.confirm()
-                        .title($translate.instant('messenger.CONFIRM_DELETE_TITLE'))
-                        .textContent($translate.instant('common.ARE_YOU_SURE'))
-                        .targetEvent(ev)
-                        .ok($translate.instant('common.YES'))
-                        .cancel($translate.instant('common.CANCEL'));
-                    $mdDialog.show(confirm).then((result) => {
-                        webClientService.deleteMessage(this.receiver, this.message);
-                    }, () => { /* do nothing */});
-                };
+                this.$onInit = function() {
 
-                this.copyToClipboard = (ev: MouseEvent) => {
-                    // Get copyable text
-                    const text = messageService.getQuoteText(this.message);
-                    if (text === null) {
-                        return;
+                    // Defaults and variables
+                    if (this.resolution == null) {
+                        this.resolution = 'low';
                     }
 
-                    // In order to copy the text to the clipboard,
-                    // put it into a temporary textarea element.
-                    const textArea = document.createElement('textarea');
-                    let toastString = 'messenger.COPY_ERROR';
-                    textArea.value = text;
-                    document.body.appendChild(textArea);
-                    textArea.focus();
-                    textArea.select();
-                    try {
-                        const successful = document.execCommand('copy');
-                        if (!successful) {
-                            $log.warn(this.logTag, 'Could not copy text to clipboard');
-                        } else {
-                            toastString = 'messenger.COPIED';
+                    // Find contact
+                    this.contact = webClientService.contacts.get(
+                        getSenderIdentity(this.message, webClientService.me.id),
+                    );
+
+                    // Show...
+                    this.isStatusMessage = this.message.isStatus;
+                    this.isContactMessage = !this.message.isStatus
+                        && webClientService.contacts.has(getSenderIdentity(this.message, webClientService.me.id));
+                    this.isGroup = this.type as threema.ReceiverType === 'group';
+                    this.isContact = this.type as threema.ReceiverType === 'contact';
+                    this.isBusinessReceiver = receiverService.isBusinessContact(this.receiver);
+
+                    this.showName = !this.message.isOutbox && this.isGroup;
+                    // show avatar only if a name is shown
+                    this.showAvatar = this.showName;
+                    this.showText = this.message.type === 'text' || this.message.caption;
+                    this.showMedia = this.message.type !== 'text';
+                    this.showState = messageService.showStatusIcon(this.message as threema.Message, this.receiver);
+                    this.showQuote = this.message.quote !== undefined;
+                    this.showVoipInfo = this.message.type === 'voipStatus';
+
+                    this.access = messageService.getAccess(this.message, this.receiver);
+
+                    this.ack = (ack: boolean) => {
+                        webClientService.ackMessage(this.receiver, this.message, ack);
+                    };
+
+                    this.quote = () => {
+                        // set message as quoted
+                        webClientService.setQuote(this.receiver, this.message);
+                    };
+
+                    this.delete = (ev) => {
+                        const confirm = $mdDialog.confirm()
+                            .title($translate.instant('messenger.CONFIRM_DELETE_TITLE'))
+                            .textContent($translate.instant('common.ARE_YOU_SURE'))
+                            .targetEvent(ev)
+                            .ok($translate.instant('common.YES'))
+                            .cancel($translate.instant('common.CANCEL'));
+                        $mdDialog.show(confirm).then((result) => {
+                            webClientService.deleteMessage(this.receiver, this.message);
+                        }, () => { /* do nothing */});
+                    };
+
+                    this.copyToClipboard = (ev: MouseEvent) => {
+                        // Get copyable text
+                        const text = messageService.getQuoteText(this.message);
+                        if (text === null) {
+                            return;
                         }
-                    } catch (err) {
-                        $log.warn(this.logTag, 'Could not copy text to clipboard:', err);
-                    }
-                    document.body.removeChild(textArea);
-
-                    // Show toast
-                    const toast = $mdToast.simple()
-                        .textContent($translate.instant(toastString))
-                        .position('bottom center');
-                    $mdToast.show(toast);
-                };
 
-                this.download = (ev) => {
-                    this.downloading = true;
-                    webClientService.requestBlob(this.message.id, this.receiver)
-                        .then((blobInfo: threema.BlobInfo) => {
-                            $rootScope.$apply(() => {
+                        // In order to copy the text to the clipboard,
+                        // put it into a temporary textarea element.
+                        const textArea = document.createElement('textarea');
+                        let toastString = 'messenger.COPY_ERROR';
+                        textArea.value = text;
+                        document.body.appendChild(textArea);
+                        textArea.focus();
+                        textArea.select();
+                        try {
+                            const successful = document.execCommand('copy');
+                            if (!successful) {
+                                $log.warn(this.logTag, 'Could not copy text to clipboard');
+                            } else {
+                                toastString = 'messenger.COPIED';
+                            }
+                        } catch (err) {
+                            $log.warn(this.logTag, 'Could not copy text to clipboard:', err);
+                        }
+                        document.body.removeChild(textArea);
+
+                        // Show toast
+                        const toast = $mdToast.simple()
+                            .textContent($translate.instant(toastString))
+                            .position('bottom center');
+                        $mdToast.show(toast);
+                    };
+
+                    this.download = (ev) => {
+                        this.downloading = true;
+                        webClientService.requestBlob(this.message.id, this.receiver)
+                            .then((blobInfo: threema.BlobInfo) => {
+                                $rootScope.$apply(() => {
+                                    this.downloading = false;
+
+                                    switch (this.message.type) {
+                                        case 'image':
+                                        case 'video':
+                                        case 'file':
+                                        case 'audio':
+                                            saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
+                                            break;
+                                        default:
+                                            $log.warn(this.logTag, 'Ignored download request for message type', this.message.type);
+                                    }
+                                });
+                            })
+                            .catch((error) => {
+                                $log.error(this.logTag, 'Error downloading blob:', error);
                                 this.downloading = false;
-
-                                switch (this.message.type) {
-                                    case 'image':
-                                    case 'video':
-                                    case 'file':
-                                    case 'audio':
-                                        saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
-                                        break;
-                                    default:
-                                        $log.warn(this.logTag, 'Ignored download request for message type', this.message.type);
-                                }
                             });
-                        })
-                        .catch((error) => {
-                            $log.error(this.logTag, 'Error downloading blob:', error);
-                            this.downloading = false;
+                    };
+
+                    this.isDownloading = () => {
+                        return this.downloading;
+                    };
+
+                    this.showHistory = (ev) => {
+                        const getEvents = () => this.message.events;
+                        $mdDialog.show({
+                            controllerAs: 'ctrl',
+                            controller: function() {
+                                this.getEvents = getEvents;
+                                this.close = () => {
+                                    $mdDialog.hide();
+                                };
+                            },
+                            template: `
+                                <md-dialog class="message-history-dialog" translate-attr="{'aria-label': 'messenger.MSG_HISTORY'}">
+                                    <form ng-cloak>
+                                        <md-toolbar>
+                                            <div class="md-toolbar-tools">
+                                                <h2 translate>messenger.MSG_HISTORY</h2>
+                                            </div>
+                                        </md-toolbar>
+                                        <md-dialog-content>
+                                            <p ng-repeat="event in ctrl.getEvents()">
+                                                <span class="event-type" ng-if="event.type === 'created'" translate>messenger.MSG_HISTORY_CREATED</span>
+                                                <span class="event-type" ng-if="event.type === 'sent'" translate>messenger.MSG_HISTORY_SENT</span>
+                                                <span class="event-type" ng-if="event.type === 'delivered'" translate>messenger.MSG_HISTORY_DELIVERED</span>
+                                                <span class="event-type" ng-if="event.type === 'read'" translate>messenger.MSG_HISTORY_READ</span>
+                                                <span class="event-type" ng-if="event.type === 'acked'" translate>messenger.MSG_HISTORY_ACKED</span>
+                                                <span class="event-type" ng-if="event.type === 'modified'" translate>messenger.MSG_HISTORY_MODIFIED</span>
+                                                {{ event.date | unixToTimestring:true }}
+                                            </p>
+                                        </md-dialog-content>
+                                        <md-dialog-actions layout="row" >
+                                            <md-button ng-click="ctrl.close()">
+                                                <span translate>common.OK</span>
+                                            </md-button>
+                                        </md-dialog-actions>
+                                    </form>
+                                </md-dialog>
+                            `,
+                            parent: angular.element(document.body),
+                            targetEvent: ev,
+                            clickOutsideToClose: true,
                         });
-                };
-
-                this.isDownloading = () => {
-                    return this.downloading;
-                };
-
-                this.showHistory = (ev) => {
-                    const getEvents = () => this.message.events;
-                    $mdDialog.show({
-                        controllerAs: 'ctrl',
-                        controller: function() {
-                            this.getEvents = getEvents;
-                            this.close = () => {
-                                $mdDialog.hide();
-                            };
-                        },
-                        template: `
-                            <md-dialog class="message-history-dialog" translate-attr="{'aria-label': 'messenger.MSG_HISTORY'}">
-                                <form ng-cloak>
-                                    <md-toolbar>
-                                        <div class="md-toolbar-tools">
-                                            <h2 translate>messenger.MSG_HISTORY</h2>
-                                        </div>
-                                    </md-toolbar>
-                                    <md-dialog-content>
-                                        <p ng-repeat="event in ctrl.getEvents()">
-                                            <span class="event-type" ng-if="event.type === 'created'" translate>messenger.MSG_HISTORY_CREATED</span>
-                                            <span class="event-type" ng-if="event.type === 'sent'" translate>messenger.MSG_HISTORY_SENT</span>
-                                            <span class="event-type" ng-if="event.type === 'delivered'" translate>messenger.MSG_HISTORY_DELIVERED</span>
-                                            <span class="event-type" ng-if="event.type === 'read'" translate>messenger.MSG_HISTORY_READ</span>
-                                            <span class="event-type" ng-if="event.type === 'acked'" translate>messenger.MSG_HISTORY_ACKED</span>
-                                            <span class="event-type" ng-if="event.type === 'modified'" translate>messenger.MSG_HISTORY_MODIFIED</span>
-                                            {{ event.date | unixToTimestring:true }}
-                                        </p>
-                                    </md-dialog-content>
-                                    <md-dialog-actions layout="row" >
-                                        <md-button ng-click="ctrl.close()">
-                                            <span translate>common.OK</span>
-                                        </md-button>
-                                    </md-dialog-actions>
-                                </form>
-                            </md-dialog>
-                        `,
-                        parent: angular.element(document.body),
-                        targetEvent: ev,
-                        clickOutsideToClose: true,
-                    });
+                    };
                 };
             }],
             link: function(scope: any, element: ng.IAugmentedJQuery, attrs) {

+ 6 - 2
src/directives/message_icon.ts

@@ -47,10 +47,14 @@ export default [
                             return null;
                     }
                 };
-                this.icon = getIcon(this.message.type);
+
+                this.$onInit = function() {
+                    this.icon = getIcon(this.message.type);
+                    this.altText = this.message.type + ' icon';
+                };
             }],
             template: `
-                <img ng-if="ctrl.icon !== null" ng-src="img/{{ ctrl.icon }}" alt="{{ ctrl.message.type }} icon" />
+                <img ng-if="ctrl.icon !== null" ng-src="img/{{ ctrl.icon }}" alt="{{ ctrl.altText }}" />
             `,
         };
     },

+ 205 - 197
src/directives/message_media.ts

@@ -20,6 +20,44 @@ import {MediaboxService} from '../services/mediabox';
 import {MessageService} from '../services/message';
 import {WebClientService} from '../services/webclient';
 
+function showAudioDialog(
+    $mdDialog: ng.material.IDialogService,
+    $log: ng.ILogService,
+    blobInfo: threema.BlobInfo,
+): void {
+    $mdDialog.show({
+        controllerAs: 'ctrl',
+        controller: function() {
+            this.cancel = () => $mdDialog.cancel();
+            this.audioSrc = bufferToUrl(
+                blobInfo.buffer,
+                blobInfo.mimetype,
+                logAdapter($log.warn, '[AudioPlayerDialog]'),
+            );
+        },
+        template: `
+            <md-dialog translate-attr="{'aria-label': 'messageTypes.AUDIO_MESSAGE'}">
+                    <md-toolbar>
+                        <div class="md-toolbar-tools">
+                            <h2 translate>messageTypes.AUDIO_MESSAGE</h2>
+                            </div>
+                    </md-toolbar>
+                    <md-dialog-content layout="row" layout-align="center">
+                        <audio controls autoplay ng-src="{{ ctrl.audioSrc | unsafeResUrl }}">
+                            Your browser does not support the <code>audio</code> element.
+                        </audio>
+                    </md-dialog-content>
+                    <md-dialog-actions layout="row" >
+                      <md-button ng-click="ctrl.cancel()">
+                         <span translate>common.OK</span>
+                      </md-button>
+                    </md-dialog-actions>
+            </md-dialog>`,
+        parent: angular.element(document.body),
+        clickOutsideToClose: true,
+    });
+}
+
 export default [
     'WebClientService',
     'MediaboxService',
@@ -53,224 +91,194 @@ export default [
             controller: [function() {
                 this.logTag = '[MessageMedia]';
 
-                this.type = this.message.type;
+                this.$onInit = function() {
+                    this.type = this.message.type;
 
-                // Downloading
-                this.downloading = false;
-                this.thumbnailDownloading = false;
-                this.downloaded = false;
+                    // Downloading
+                    this.downloading = false;
+                    this.thumbnailDownloading = false;
+                    this.downloaded = false;
 
-                // Uploading
-                this.uploading = this.message.temporaryId !== undefined
-                    && this.message.temporaryId !== null;
+                    // Uploading
+                    this.uploading = this.message.temporaryId !== undefined
+                        && this.message.temporaryId !== null;
 
-                // AnimGIF detection
-                this.isAnimGif = !this.uploading
-                    && (this.message as threema.Message).type === 'file'
-                    && (this.message as threema.Message).file.type === 'image/gif';
+                    // AnimGIF detection
+                    this.isAnimGif = !this.uploading
+                        && (this.message as threema.Message).type === 'file'
+                        && (this.message as threema.Message).file.type === 'image/gif';
 
-                // Preview thumbnail
-                let thumbnailPreviewUri = null;
-                this.getThumbnailPreviewUri = () => {
-                    // Cache thumbnail preview URI
-                    if (thumbnailPreviewUri === null && hasValue(this.message) && hasValue(this.message.thumbnail)) {
-                        thumbnailPreviewUri = bufferToUrl(
-                            (this.message as threema.Message).thumbnail.preview,
-                            webClientService.appCapabilities.imageFormat.thumbnail,
-                            logAdapter($log.warn, this.logTag),
-                        );
-                    }
-                    return thumbnailPreviewUri;
-                };
+                    // Preview thumbnail
+                    let thumbnailPreviewUri = null;
+                    this.getThumbnailPreviewUri = () => {
+                        // Cache thumbnail preview URI
+                        if (thumbnailPreviewUri === null
+                                && hasValue(this.message) && hasValue(this.message.thumbnail)) {
+                            thumbnailPreviewUri = bufferToUrl(
+                                (this.message as threema.Message).thumbnail.preview,
+                                webClientService.appCapabilities.imageFormat.thumbnail,
+                                logAdapter($log.warn, this.logTag),
+                            );
+                        }
+                        return thumbnailPreviewUri;
+                    };
 
-                // Thumbnail loading
-                //
-                // Do not show thumbnail in file messages (except anim gif).
-                // If a thumbnail in file messages are available, the thumbnail
-                // will be shown in the file circle
-                this.showThumbnail = this.message.thumbnail !== undefined
-                    && ((this.message as threema.Message).type !== 'file' || this.isAnimGif);
-                this.thumbnail = null;
-                this.thumbnailFormat = webClientService.appCapabilities.imageFormat.thumbnail;
-                if (this.message.thumbnail !== undefined) {
-                    this.thumbnailStyle = {
-                        width: this.message.thumbnail.width + 'px',
-                        height: this.message.thumbnail.height + 'px' };
-                }
+                    // Thumbnail loading
+                    //
+                    // Do not show thumbnail in file messages (except anim gif).
+                    // If a thumbnail in file messages are available, the thumbnail
+                    // will be shown in the file circle
+                    this.showThumbnail = this.message.thumbnail !== undefined
+                        && ((this.message as threema.Message).type !== 'file' || this.isAnimGif);
+                    this.thumbnail = null;
+                    this.thumbnailFormat = webClientService.appCapabilities.imageFormat.thumbnail;
+                    if (this.message.thumbnail !== undefined) {
+                        this.thumbnailStyle = {
+                            width: this.message.thumbnail.width + 'px',
+                            height: this.message.thumbnail.height + 'px' };
+                    }
 
-                let loadingThumbnailTimeout = null;
+                    let loadingThumbnailTimeout = null;
 
-                this.wasInView = false;
-                this.thumbnailInView = (inView: boolean) => {
-                    if (this.message.thumbnail === undefined
-                            || this.wasInView === inView) {
-                        // do nothing
-                        return;
-                    }
-                    this.wasInView = inView;
+                    this.wasInView = false;
+                    this.thumbnailInView = (inView: boolean) => {
+                        if (this.message.thumbnail === undefined
+                                || this.wasInView === inView) {
+                            // do nothing
+                            return;
+                        }
+                        this.wasInView = inView;
 
-                    if (!inView) {
-                        $timeout.cancel(loadingThumbnailTimeout);
-                        this.thumbnailDownloading = false;
-                        this.thumbnail = null;
-                    } else {
-                        if (this.thumbnail === null) {
-                            const setThumbnail = (buf: ArrayBuffer) => {
-                                this.thumbnail = bufferToUrl(
-                                    buf,
-                                    webClientService.appCapabilities.imageFormat.thumbnail,
-                                    logAdapter($log.warn, this.logTag),
-                                );
-                            };
+                        if (!inView) {
+                            $timeout.cancel(loadingThumbnailTimeout);
+                            this.thumbnailDownloading = false;
+                            this.thumbnail = null;
+                        } else {
+                            if (this.thumbnail === null) {
+                                const setThumbnail = (buf: ArrayBuffer) => {
+                                    this.thumbnail = bufferToUrl(
+                                        buf,
+                                        webClientService.appCapabilities.imageFormat.thumbnail,
+                                        logAdapter($log.warn, this.logTag),
+                                    );
+                                };
 
-                            if (this.message.thumbnail.img !== undefined) {
-                                setThumbnail(this.message.thumbnail.img);
-                                return;
-                            } else {
-                                this.thumbnailDownloading = true;
-                                loadingThumbnailTimeout = $timeout(() => {
-                                    webClientService
-                                        .requestThumbnail(this.receiver, this.message)
-                                        .then((img) => $timeout(() => {
-                                            setThumbnail(img);
-                                            this.thumbnailDownloading = false;
-                                        }));
-                                }, 1000);
+                                if (this.message.thumbnail.img !== undefined) {
+                                    setThumbnail(this.message.thumbnail.img);
+                                    return;
+                                } else {
+                                    this.thumbnailDownloading = true;
+                                    loadingThumbnailTimeout = $timeout(() => {
+                                        webClientService
+                                            .requestThumbnail(this.receiver, this.message)
+                                            .then((img) => $timeout(() => {
+                                                setThumbnail(img);
+                                                this.thumbnailDownloading = false;
+                                            }));
+                                    }, 1000);
+                                }
                             }
                         }
+                    };
+
+                    // For locations, retrieve the coordinates
+                    this.location = null;
+                    if (this.message.location !== undefined) {
+                        this.location = this.message.location;
+                        this.downloaded = true;
                     }
-                };
 
-                // For locations, retrieve the coordinates
-                this.location = null;
-                if (this.message.location !== undefined) {
-                    this.location = this.message.location;
-                    this.downloaded = true;
-                }
+                    // Open map link in new window using mapLink-filter
+                    this.openMapLink = () => {
+                        $window.open($filter<any>('mapLink')(this.location), '_blank');
+                    };
 
-                // Open map link in new window using mapLink-filter
-                this.openMapLink = () => {
-                    $window.open($filter<any>('mapLink')(this.location), '_blank');
-                };
+                    // Play a Audio file in a dialog
+                    this.playAudio = (blobInfo: threema.BlobInfo) => showAudioDialog($mdDialog, $log, blobInfo);
 
-                // Play a Audio file in a dialog
-                this.playAudio = (blobInfo: threema.BlobInfo) => {
-                    $mdDialog.show({
-                        controllerAs: 'ctrl',
-                        controller: function() {
-                            this.blobBuffer = blobInfo.buffer;
-                            this.mimeType = blobInfo.mimetype;
-                            this.cancel = () => {
-                                $mdDialog.cancel();
-                            };
-                        },
-                        template: `
-                            <md-dialog translate-attr="{'aria-label': 'messageTypes.AUDIO_MESSAGE'}">
-                                    <md-toolbar>
-                                        <div class="md-toolbar-tools">
-                                            <h2 translate>messageTypes.AUDIO_MESSAGE</h2>
-                                            </div>
-                                    </md-toolbar>
-                                    <md-dialog-content layout="row" layout-align="center">
-                                        <audio
-                                            controls
-                                            autoplay ng-src="{{ ctrl.blobBuffer | bufferToUrl:ctrl.mimeType }}">
-                                            Your browser does not support the <code>audio</code> element.
-                                        </audio>
-                                    </md-dialog-content>
-                                    <md-dialog-actions layout="row" >
-                                      <md-button ng-click="ctrl.cancel()">
-                                         <span translate>common.OK</span>
-                                      </md-button>
-                                    </md-dialog-actions>
-                            </md-dialog>`,
-                        parent: angular.element(document.body),
-                        clickOutsideToClose: true,
-                    });
-                };
-
-                // Download function
-                this.download = () => {
-                    $log.debug(this.logTag, 'Download blob');
-                    if (this.downloading) {
-                        $log.debug(this.logTag, 'Download already in progress...');
-                        return;
-                    }
-                    const message: threema.Message = this.message;
-                    const receiver: threema.Receiver = this.receiver;
-                    this.downloading = true;
-                    webClientService.requestBlob(message.id, receiver)
-                        .then((blobInfo: threema.BlobInfo) => {
-                            $rootScope.$apply(() => {
-                                $log.debug(this.logTag, 'Blob loaded');
-                                this.downloading = false;
-                                this.downloaded = true;
+                    // Download function
+                    this.download = () => {
+                        $log.debug(this.logTag, 'Download blob');
+                        if (this.downloading) {
+                            $log.debug(this.logTag, 'Download already in progress...');
+                            return;
+                        }
+                        const message: threema.Message = this.message;
+                        const receiver: threema.Receiver = this.receiver;
+                        this.downloading = true;
+                        webClientService.requestBlob(message.id, receiver)
+                            .then((blobInfo: threema.BlobInfo) => {
+                                $rootScope.$apply(() => {
+                                    $log.debug(this.logTag, 'Blob loaded');
+                                    this.downloading = false;
+                                    this.downloaded = true;
 
-                                switch (this.message.type) {
-                                    case 'image':
-                                        const caption = message.caption || '';
-                                        mediaboxService.setMedia(
-                                            blobInfo.buffer,
-                                            blobInfo.filename,
-                                            blobInfo.mimetype,
-                                            caption,
-                                        );
-                                        break;
-                                    case 'video':
-                                        saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
-                                        break;
-                                    case 'file':
-                                        if (this.message.file.type === 'image/gif') {
-                                            // Show inline
-                                            this.blobBufferUrl = bufferToUrl(
+                                    switch (this.message.type) {
+                                        case 'image':
+                                            const caption = message.caption || '';
+                                            mediaboxService.setMedia(
                                                 blobInfo.buffer,
-                                                'image/gif',
-                                                logAdapter($log.warn, this.logTag),
+                                                blobInfo.filename,
+                                                blobInfo.mimetype,
+                                                caption,
                                             );
-                                            // Hide thumbnail
-                                            this.showThumbnail = false;
-                                        } else {
+                                            break;
+                                        case 'video':
                                             saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
-                                        }
-                                        break;
-                                    case 'audio':
-                                        // Show inline
-                                        this.playAudio(blobInfo);
-                                        break;
-                                    default:
-                                        $log.warn(this.logTag,
-                                            'Ignored download request for message type', this.message.type);
-                                }
-                            });
-                        })
-                        .catch((error) => {
-                            $rootScope.$apply(() => {
-                                this.downloading = false;
-                                let contentString;
-                                switch (error) {
-                                    case 'blobDownloadFailed':
-                                        contentString = 'error.BLOB_DOWNLOAD_FAILED';
-                                        break;
-                                    case 'blobDecryptFailed':
-                                        contentString = 'error.BLOB_DECRYPT_FAILED';
-                                        break;
-                                    default:
-                                        contentString = 'error.ERROR_OCCURRED';
-                                        break;
-                                }
-                                const confirm = $mdDialog.alert()
-                                    .title($translate.instant('common.ERROR'))
-                                    .textContent($translate.instant(contentString))
-                                    .ok($translate.instant('common.OK'));
-                                $mdDialog.show(confirm);
+                                            break;
+                                        case 'file':
+                                            if (this.message.file.type === 'image/gif') {
+                                                // Show inline
+                                                this.blobBufferUrl = bufferToUrl(
+                                                    blobInfo.buffer,
+                                                    'image/gif',
+                                                    logAdapter($log.warn, this.logTag),
+                                                );
+                                                // Hide thumbnail
+                                                this.showThumbnail = false;
+                                            } else {
+                                                saveAs(new Blob([blobInfo.buffer]), blobInfo.filename);
+                                            }
+                                            break;
+                                        case 'audio':
+                                            // Show inline
+                                            this.playAudio(blobInfo);
+                                            break;
+                                        default:
+                                            $log.warn(this.logTag,
+                                                'Ignored download request for message type', this.message.type);
+                                    }
+                                });
+                            })
+                            .catch((error) => {
+                                $rootScope.$apply(() => {
+                                    this.downloading = false;
+                                    let contentString;
+                                    switch (error) {
+                                        case 'blobDownloadFailed':
+                                            contentString = 'error.BLOB_DOWNLOAD_FAILED';
+                                            break;
+                                        case 'blobDecryptFailed':
+                                            contentString = 'error.BLOB_DECRYPT_FAILED';
+                                            break;
+                                        default:
+                                            contentString = 'error.ERROR_OCCURRED';
+                                            break;
+                                    }
+                                    const confirm = $mdDialog.alert()
+                                        .title($translate.instant('common.ERROR'))
+                                        .textContent($translate.instant(contentString))
+                                        .ok($translate.instant('common.OK'));
+                                    $mdDialog.show(confirm);
+                                });
                             });
-                        });
-                };
+                    };
 
-                this.isDownloading = () => {
-                    return this.downloading
-                        || this.thumbnailDownloading
-                        || (this.showDownloading && this.showDownloading());
+                    this.isDownloading = () => {
+                        return this.downloading
+                            || this.thumbnailDownloading
+                            || (this.showDownloading && this.showDownloading());
+                    };
                 };
             }],
             templateUrl: 'directives/message_media.html',

+ 1 - 1
src/directives/message_menu.html

@@ -1,6 +1,6 @@
 <!-- Non status messages -->
 <md-menu md-position-mode="target-right target" md-offset="0 45">
-    <md-button aria-label="Open menu" class="md-icon-button" ng-click="$mdOpenMenu($event)">
+    <md-button aria-label="Open menu" class="md-icon-button" ng-click="$mdMenu.open($event)">
         <i class="material-icons md-dark md-24">more_vert</i>
     </md-button>
     <md-menu-content width="1">

+ 14 - 12
src/directives/message_meta.ts

@@ -26,20 +26,22 @@ export default [
             },
             controllerAs: 'ctrl',
             controller: [function() {
-                const msg = this.message as threema.Message;
+                this.$onInit = function() {
+                    const msg = this.message as threema.Message;
 
-                this.type = msg.type;
-                this.isGif = msg.file !== undefined && msg.file.type === 'image/gif';
+                    this.type = msg.type;
+                    this.isGif = msg.file !== undefined && msg.file.type === 'image/gif';
 
-                // For audio, video or voip call, retrieve the duration
-                this.duration = null;
-                if (this.message.audio !== undefined) {
-                    this.duration = this.message.audio.duration;
-                } else if (this.message.video !== undefined) {
-                    this.duration = this.message.video.duration;
-                } else if (this.message.voip !== undefined && this.message.voip.duration) {
-                    this.duration = this.message.voip.duration;
-                }
+                    // For audio, video or voip call, retrieve the duration
+                    this.duration = null;
+                    if (msg.audio !== undefined) {
+                        this.duration = msg.audio.duration;
+                    } else if (msg.video !== undefined) {
+                        this.duration = msg.video.duration;
+                    } else if (msg.voip !== undefined && msg.voip.duration) {
+                        this.duration = msg.voip.duration;
+                    }
+                };
             }],
             template: `
                 <span ng-if="ctrl.isGif" class="message-meta-item">GIF</span>

+ 22 - 26
src/directives/message_text.ts

@@ -31,35 +31,31 @@ export default [
             controllerAs: 'ctrl',
             controller: ['WebClientService', function(webClientService: WebClientService) {
                 // Get text depending on type
-                let rawText = null;
-                const message = this.message as threema.Message;
-                switch (message.type) {
-                    case 'text':
-                        rawText = message.body;
-                        break;
-                    case 'location':
-                        rawText = message.location.description;
-                        break;
-                    case 'file':
-                        // Prefer caption for file messages, if available
-                        if (message.caption && message.caption.length > 0) {
-                            rawText = message.caption;
-                        } else {
-                            rawText = message.file.name;
-                        }
-                        break;
-                    default:
-                        rawText = message.caption;
-                        break;
-                }
-
-                // Escaping will be done in the HTML using filters
-                this.text = rawText;
-                if (this.multiLine === undefined) {
-                    this.multiLine = true;
+                function getText(message: threema.Message) {
+                    switch (message.type) {
+                        case 'text':
+                            return message.body;
+                        case 'location':
+                            return message.location.description;
+                        case 'file':
+                            // Prefer caption for file messages, if available
+                            if (message.caption && message.caption.length > 0) {
+                                return message.caption;
+                            }
+                            return message.file.name;
+                    }
+                    return message.caption;
                 }
 
                 this.enlargeSingleEmoji = webClientService.appConfig.largeSingleEmoji;
+
+                this.$onInit = function() {
+                    // Escaping will be done in the HTML using filters
+                    this.text = getText(this.message);
+                    if (this.multiLine === undefined) {
+                        this.multiLine = true;
+                    }
+                };
             }],
             template: `
                 <span click-action

+ 7 - 5
src/directives/message_voip_status.ts

@@ -27,11 +27,13 @@ export default [
             },
             controllerAs: 'ctrl',
             controller: [function() {
-                this.status = this.message.voip.status;
-                this.duration = this.message.voip.duration;
-                this.reason = this.message.voip.reason;
-                this.incoming = !this.message.isOutbox;
-                this.outgoing = this.message.isOutbox;
+                this.$onInit = function() {
+                    this.status = this.message.voip.status;
+                    this.duration = this.message.voip.duration;
+                    this.reason = this.message.voip.reason;
+                    this.incoming = !this.message.isOutbox;
+                    this.outgoing = this.message.isOutbox;
+                };
             }],
             template: `
                 <p ng-if="ctrl.status === 1">

+ 34 - 31
src/directives/verification_level.ts

@@ -26,40 +26,43 @@ export default [
             },
             controllerAs: 'ctrl',
             controller: [function() {
-                const contact: threema.ContactReceiver = this.contact;
+                this.$onInit = function() {
+                    const contact: threema.ContactReceiver = this.contact;
 
-                let label;
-                switch (contact.verificationLevel) {
-                    case 1:
-                        this.cls = 'level1';
-                        label = 'VERIFICATION_LEVEL1_EXPLAIN';
-                        break;
-                    case 2:
-                        this.cls = 'level2';
-                        if (contact.isWork) {
-                            label = 'VERIFICATION_LEVEL2_WORK_EXPLAIN';
-                        } else {
-                            label = 'VERIFICATION_LEVEL2_EXPLAIN';
-                        }
-                        break;
-                    case 3:
-                        this.cls = 'level3';
-                        label = 'VERIFICATION_LEVEL3_EXPLAIN';
-                        break;
-                    default:
-                        /* ignore, handled on next line */
-                }
+                    let label;
+                    switch (contact.verificationLevel) {
+                        case 1:
+                            this.cls = 'level1';
+                            label = 'VERIFICATION_LEVEL1_EXPLAIN';
+                            break;
+                        case 2:
+                            this.cls = 'level2';
+                            if (contact.isWork) {
+                                label = 'VERIFICATION_LEVEL2_WORK_EXPLAIN';
+                            } else {
+                                label = 'VERIFICATION_LEVEL2_EXPLAIN';
+                            }
+                            break;
+                        case 3:
+                            this.cls = 'level3';
+                            label = 'VERIFICATION_LEVEL3_EXPLAIN';
+                            break;
+                        default:
+                            /* ignore, handled on next line */
+                    }
 
-                if (label === undefined) {
-                    $log.error('invalid verification level', this.level);
-                    return;
-                }
+                    if (label === undefined) {
+                        $log.error('invalid verification level', this.level);
+                        return;
+                    }
 
-                if (contact.isWork) {
-                    // append work class
-                    this.cls += ' work';
-                }
-                this.description = $translate.instant('messenger.' + label);
+                    if (contact.isWork) {
+                        // append work class
+                        this.cls += ' work';
+                    }
+
+                    this.description = $translate.instant('messenger.' + label);
+                };
             }],
             template: `
                 <span class="verification-dots {{ctrl.cls}}" title="{{ctrl.description}}">

+ 9 - 0
src/filters.ts

@@ -266,6 +266,7 @@ angular.module('3ema.filters', [])
         }
     };
 }])
+
 .filter('mapLink', function() {
     return function(location: threema.LocationInfo) {
         return 'https://www.openstreetmap.org/?mlat='
@@ -273,6 +274,7 @@ angular.module('3ema.filters', [])
             + location.lon;
     };
 })
+
 /**
  * Convert message state to material icon class.
  */
@@ -483,4 +485,11 @@ angular.module('3ema.filters', [])
     };
 }])
 
+/**
+ * Mark data as trusted.
+ */
+.filter('unsafeResUrl', ['$sce', function($sce: ng.ISCEService) {
+    return $sce.trustAsResourceUrl;
+}])
+
 ;

+ 28 - 0
src/helpers/messages.ts

@@ -0,0 +1,28 @@
+/**
+ * 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/>.
+ */
+
+// This file contains helper functions related to messages.
+// Try to keep all functions pure!
+
+/**
+ * Return the sending identity of a message.
+ */
+export function getSenderIdentity(message: threema.Message, myId: string): string | null {
+    if (message.isOutbox) { return myId; }
+    if (message.partnerId != null) { return message.partnerId; }
+    return null;
+}

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

@@ -9,7 +9,7 @@
     <battery-status></battery-status>
 
     <md-menu md-position-mode="target-right target" md-offset="0 45">
-        <md-button aria-label="Open menu" class="md-icon-button" ng-click="$mdOpenMenu($event)">
+        <md-button aria-label="Open menu" class="md-icon-button" ng-click="$mdMenu.open($event)">
             <i class="material-icons md-light md-24">more_vert</i>
         </md-button>
         <md-menu-content width="4">

+ 1 - 1
src/partials/messenger.receiver/contact.html

@@ -101,7 +101,7 @@
 		<md-card-content>
 			<ul class="group-list">
 				<li ng-repeat="distributionListReceiver in ctrl.inDistributionLists">
-					<eee-distribution-list-badge eee-distribution-list-receiver="distributionListReceiver" eee-contact-receiver="ctrl.receiver"/>
+					<eee-distribution-list-badge eee-distribution-list-receiver="distributionListReceiver"/>
 				</li>
 			</ul>
 		</md-card-content>

部分文件因文件數量過多而無法顯示